Button

button.svelte

Buttons that should be used primarily for actions that require attention.

Demo

LINK BUTTON
Usage
001 <script lang="ts" module>
002 import Button from '$lib/components/button.svelte';
003 </script>
004
005 <div class="flex flex-col">
006 <Button onclick={() => console.log('primary button action')}>PRIMARY BUTTON</Button>
007 <Button variant="secondary" onclick={() => console.log('secondary button action')}
008 >SECONDARY BUTTON
009 </Button>
010 <Button onclick={() => console.log('disabled button action')} disabled={true}
011 >DISABLED BUTTON
012 </Button>
013 <Button variant="primary" href="https://svelte.dev" target="_blank" rel="noopener noreferrer"
014 >LINK BUTTON
015 </Button>
016 </div>
017
button.svelte
001 <script lang="ts" module>
002 import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
003
004 export type ButtonVariant = 'primary' | 'secondary';
005
006 /**
007 * Polymorphic: renders `<a>` when `href` is set, `<button>` otherwise. The
008 * `href` discriminator narrows the allowed attributes — anchor-only props
009 * (target, rel, …) are only accepted on the anchor variant.
010 */
011 export type ButtonProps =
012 | (HTMLButtonAttributes & {
013 ref?: HTMLElement | null;
014 variant?: ButtonVariant;
015 href?: undefined;
016 })
017 | (HTMLAnchorAttributes & { ref?: HTMLElement | null; variant?: ButtonVariant; href: string });
018 </script>
019
020 <script lang="ts">
021 let {
022 class: className,
023 variant = 'primary',
024 ref = $bindable(null),
025 href,
026 children,
027 ...restProps
028 }: ButtonProps = $props();
029 </script>
030
031 {#if href}
032 <a
033 class={`button ${variant} ${className ?? ''}`}
034 bind:this={ref}
035 {href}
036 {...restProps as HTMLAnchorAttributes}
037 >
038 {@render children?.()}
039 </a>
040 {:else}
041 <button
042 class={`button ${variant} ${className ?? ''}`}
043 bind:this={ref}
044 {...restProps as HTMLButtonAttributes}
045 >
046 {@render children?.()}
047 </button>
048 {/if}
049
050 <style>
051 .button {
052 box-sizing: border-box;
053 border: none;
054 border-radius: 0;
055 margin: 0;
056 padding: var(--line) 2ch;
057 text-align: center;
058 text-transform: uppercase;
059 text-decoration: none;
060 background-clip: padding-box;
061 display: inline-block;
062 width: 100%;
063 text-align: center;
064 font-family: var(--font-family-mono);
065 font-weight: 400;
066 letter-spacing: 0.05em;
067 transition: all 0.2s ease-in-out;
068 cursor: pointer;
069 }
070
071 .button:disabled,
072 .button[aria-disabled='true'] {
073 cursor: not-allowed;
074 pointer-events: none;
075 opacity: 0.5;
076 }
077
078 .button.primary {
079 background-color: var(--button-primary-bg);
080 color: var(--button-primary-fg);
081 }
082
083 .button.secondary {
084 background-color: var(--surface-base);
085 color: var(--text-primary);
086 box-shadow: inset 0 0 0 1px var(--border-default);
087 }
088
089 .button:hover {
090 color: var(--button-primary-fg);
091 background-color: var(--focus-ring);
092 box-shadow: none;
093 }
094
095 .button:focus-visible {
096 outline: none;
097 color: var(--button-primary-fg);
098 background-color: var(--focus-ring);
099 box-shadow: none;
100 }
101
102 /* :focus-visible keeps the highlight only for keyboard focus — a mouse click
103 doesn't leave the ring painted, so the hover color doesn't linger. */
104 </style>
105