Svelte 5 component · fractalsvelte-ui · Actions
Button
An interactive trigger for user actions with support for multiple visual variants, sizes, shapes, and an integrated loading state. Accepts leading and trailing snippets and transparently renders as an anchor when an href is supplied.
Install
Button is a Svelte 5 button component in @fractaldesign/fractalsvelte-ui. It works in SvelteKit and in any Svelte 5 app.
pnpm add @fractaldesign/fractalsvelte-uiImport
import { Button } from '@fractaldesign/fractalsvelte-ui';Usage
<Button variant={'primary'} size={'md'} shape={'rounded'}>Click me</Button>Props
Also accepts every attribute of HTMLButtonAttributes (without type), spread onto
the root element.
| Name | Type | Default |
|---|---|---|
variant | Variant 'primary' | 'secondary' | 'soft' | 'outline' | 'ghost' | 'destructive' | 'surface' | 'primary' |
size | Size 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' |
shape | Shape 'square' | 'rounded' | 'pill' | 'rounded' |
type | BtnType | any 'button' | 'submit' | 'reset' | 'button' |
loading | boolean | false |
disabled | boolean | null | false |
href | string | — |
class | string | '' |
Snippets
| Name | Type | Default |
|---|---|---|
children | Snippet | — |
leading | Snippet | — |
trailing | Snippet | — |
Source
Show the Button.svelte source
<script lang="ts">
import type { Snippet } from 'svelte';
import type { HTMLButtonAttributes, HTMLAnchorAttributes } from 'svelte/elements';
import type { Variant, Size, Shape, BtnType } from '../../types';
interface Props extends Omit<HTMLButtonAttributes, 'type'> {
variant?: Variant;
size?: Size;
shape?: Shape;
type?: BtnType | any;
loading?: boolean;
disabled?: boolean | null;
href?: string;
class?: string;
children?: Snippet;
leading?: Snippet;
trailing?: Snippet;
}
let {
variant = 'primary',
size = 'md',
shape = 'rounded',
type = 'button',
loading = false,
disabled = false,
href,
class: className = '',
children,
leading,
trailing,
...restProps
}: Props = $props();
const isAnchor = $derived(Boolean(href));
const inert = $derived(disabled || loading);
</script>
{#if isAnchor}
<a
{href}
class="f-btn {className}"
data-variant={variant}
data-size={size}
data-shape={shape}
data-state={loading ? 'loading' : undefined}
aria-busy={loading || undefined}
aria-disabled={inert || undefined}
tabindex={inert ? -1 : undefined}
{...restProps as HTMLAnchorAttributes}
>
{#if loading}
<span class="f-btn-spinner" aria-hidden="true"></span>
{:else if leading}
{@render leading()}
{/if}
{#if children}
{@render children()}
{/if}
{#if trailing && !loading}
{@render trailing()}
{/if}
</a>
{:else}
<button
{type}
class="f-btn {className}"
data-variant={variant}
data-size={size}
data-shape={shape}
data-state={loading ? 'loading' : undefined}
disabled={inert}
aria-busy={loading || undefined}
{...restProps}
>
{#if loading}
<span class="f-btn-spinner" aria-hidden="true"></span>
{:else if leading}
{@render leading()}
{/if}
{#if children}
{@render children()}
{/if}
{#if trailing && !loading}
{@render trailing()}
{/if}
</button>
{/if}
<style>
@layer fractalsvelte-ui.components {
.f-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--f-space-xs);
font-family: inherit;
font-weight: 500;
line-height: 1;
white-space: nowrap;
cursor: pointer;
user-select: none;
text-decoration: none;
border: 1px solid transparent;
box-sizing: border-box;
transition: background var(--f-duration-fast) var(--f-ease),
border-color var(--f-duration-fast) var(--f-ease),
color var(--f-duration-fast) var(--f-ease),
box-shadow var(--f-duration-fast) var(--f-ease),
transform var(--f-duration-fast) var(--f-ease);
}
.f-btn:focus-visible {
outline: 2px solid var(--f-ring);
outline-offset: 2px;
}
.f-btn:active:not(:disabled):not([aria-disabled='true']) {
transform: scale(0.98);
}
.f-btn:disabled,
.f-btn[aria-disabled='true'] {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
/* Shapes */
.f-btn[data-shape='rounded'] {
border-radius: var(--f-radius-md);
}
.f-btn[data-shape='pill'] {
border-radius: var(--f-radius-full);
}
.f-btn[data-shape='square'] {
border-radius: 0;
}
/* Sizes */
.f-btn[data-size='xs'] {
padding: var(--f-space-3xs) var(--f-space-xs);
font-size: var(--f-text-xs);
height: 24px;
}
.f-btn[data-size='sm'] {
padding: var(--f-space-2xs) var(--f-space-sm);
font-size: var(--f-text-sm);
height: 32px;
}
.f-btn[data-size='md'] {
padding: var(--f-space-xs) var(--f-space-md);
font-size: var(--f-text-base);
height: 40px;
}
.f-btn[data-size='lg'] {
padding: var(--f-space-sm) var(--f-space-lg);
font-size: var(--f-text-lg);
height: 48px;
}
.f-btn[data-size='xl'] {
padding: var(--f-space-md) var(--f-space-xl);
font-size: var(--f-text-xl);
height: 56px;
}
/* Variants */
.f-btn[data-variant='primary'] {
background: var(--f-primary);
color: var(--f-primary-text);
border-color: var(--f-primary);
}
.f-btn[data-variant='primary']:hover:not(:disabled) {
background: var(--f-primary-hover);
border-color: var(--f-primary-hover);
}
.f-btn[data-variant='secondary'] {
background: var(--f-surface);
color: var(--f-text);
border-color: var(--f-border);
box-shadow: var(--f-shadow-xs);
}
.f-btn[data-variant='secondary']:hover:not(:disabled) {
background: var(--f-hover);
border-color: var(--f-border-strong);
}
.f-btn[data-variant='soft'] {
background: var(--f-primary-subtle);
color: var(--f-primary);
border-color: transparent;
}
.f-btn[data-variant='soft']:hover:not(:disabled) {
background: var(--f-primary-subtle);
filter: brightness(0.95);
}
.f-btn[data-variant='outline'] {
background: transparent;
color: var(--f-text);
border-color: var(--f-border-strong);
}
.f-btn[data-variant='outline']:hover:not(:disabled) {
background: var(--f-hover);
color: var(--f-primary);
border-color: var(--f-primary);
}
.f-btn[data-variant='ghost'] {
background: transparent;
color: var(--f-text-muted);
border-color: transparent;
}
.f-btn[data-variant='ghost']:hover:not(:disabled) {
background: var(--f-hover);
color: var(--f-text);
}
.f-btn[data-variant='destructive'] {
background: var(--f-danger);
color: #ffffff;
border-color: var(--f-danger);
}
.f-btn[data-variant='destructive']:hover:not(:disabled) {
filter: brightness(0.9);
}
.f-btn[data-variant='surface'] {
background: var(--f-raised);
color: var(--f-text);
border-color: var(--f-border-subtle);
}
.f-btn[data-variant='surface']:hover:not(:disabled) {
background: var(--f-hover);
}
/* Spinner */
.f-btn-spinner {
width: 14px;
height: 14px;
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: f-btn-spin 0.6s linear infinite;
}
@keyframes f-btn-spin {
to {
transform: rotate(360deg);
}
}
}
</style>Related components
ButtonGroup A clustered group container that attaches adjacent buttons seamlessly with merged borders or separates them…CloseButton A standardized dismissal trigger for dialogs, drawers, toasts, and alerts.CopyButton A one-click clipboard copy utility that transitions into a confirmed checkmark state upon copying.HoldToConfirm A high-stakes press-and-hold confirmation button with animated progress fill.IconButton A compact icon-only action button with accessible screen-reader labelling and hover tooltips.LinkButton A semantic navigation anchor styled with button dimensions and hover aesthetics.