# Button

> The foundational push-button: six variants, three sizes, a loading state, and a polymorphic href/anchor mode.

- Package: `fancy-ui-svelte` (Svelte 5 + Tailwind CSS v4)
- Category: Actions
- Tags: button, cta, actions, form, link, loading, icon
- Docs: https://fancy-ui.rama.app/docs/components/button
- Source: https://github.com/RamaHerbin/fancy-ui/tree/main/src/lib/fancy-ui/button

## Import

```svelte
<script lang="ts">
  import { Button } from 'fancy-ui-svelte';
</script>
```

Setup (once per project): `npm install fancy-ui-svelte`, then add `@import "fancy-ui-svelte/tailwind.css";` after `@import "tailwindcss";` in your main CSS file. Full guide: https://fancy-ui.rama.app/llms.txt

React: `import { Button } from 'fancy-ui-react';` (same prop names; `class` becomes `className`, snippets become `ReactNode`).

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"primary" \| "secondary" \| "outline" \| "ghost" \| "accent" \| "destructive"` | `"primary"` | Visual treatment |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Padding / font-size / radius scale |
| `type` | `"button" \| "submit" \| "reset"` | `"button"` | Native type; ignored once href renders an anchor instead |
| `disabled` | `boolean` | `false` | Greys the button out and blocks activation |
| `loading` | `boolean` | `false` | Spinner in place of iconStart, aria-busy, inert to activation on both the button and anchor branch — does not dim the button |
| `href` | `string` | — | Renders an <a> instead of a <button> when set |
| `target` | `string` | — | Anchor target. "_blank" forces a safe rel |
| `rel` | `string` | — | Anchor rel, widened rather than replaced when target="_blank" |
| `fullWidth` | `boolean` | `false` | Stretches the button to its container's width |
| `label` | `string` | — | Accessible name for a button whose content is icon-only |
| `onclick` | `(event: MouseEvent) => void` | — | Fires on activation; never called while disabled or loading |
| `class` | `string` | — | Additional CSS classes |
| `ref` | `HTMLButtonElement \| HTMLAnchorElement \| null` | `null` | Bindable element reference |
| `sound` | `boolean` | `false` | Plays the press cue on activation, once the user has enabled sound |

## Snippets

| Snippet | Description |
| --- | --- |
| `children` | The button's label / content |
| `iconStart` | Rendered before the label; replaced by the spinner while loading |
| `iconEnd` | Rendered after the label |

## Examples

### Basic Usage

```svelte
<script lang="ts">
	import { Button } from "fancy-ui-svelte";

	let count = $state(0);
</script>

<Button sound onclick={() => (count += 1)}>Clicked {count} {count === 1 ? "time" : "times"}</Button>
```

### Variants

All six visual treatments.

```svelte
<script lang="ts">
	import { Button } from "fancy-ui-svelte";
	import type { ButtonVariant } from "fancy-ui-svelte";

	const variants: { variant: ButtonVariant; label: string }[] = [
		{ variant: "primary", label: "Primary" },
		{ variant: "secondary", label: "Secondary" },
		{ variant: "outline", label: "Outline" },
		{ variant: "ghost", label: "Ghost" },
		{ variant: "accent", label: "Accent" },
		{ variant: "destructive", label: "Destructive" },
	];
</script>

<div class="flex flex-wrap items-center gap-3">
	{#each variants as { variant, label } (variant)}
		<Button sound {variant}>{label}</Button>
	{/each}
</div>
```

### Sizes

Small, medium, and large.

```svelte
<script lang="ts">
	import { Button } from "fancy-ui-svelte";
</script>

<div class="flex flex-wrap items-center gap-3">
	<Button sound size="sm">Small</Button>
	<Button sound size="md">Medium</Button>
	<Button sound size="lg">Large</Button>
</div>
```

### Loading State

Spinner in place of the icon, activation blocked, no dimming.

```svelte
<script lang="ts">
	import { Button } from "fancy-ui-svelte";

	let saving = $state(false);

	async function save() {
		saving = true;
		await new Promise((resolve) => setTimeout(resolve, 1200));
		saving = false;
	}
</script>

<Button sound loading={saving} onclick={save}>
	{saving ? "Saving..." : "Save changes"}
</Button>
```

### With Icons

iconStart and iconEnd snippets.

```svelte
<script lang="ts">
	import { Button } from "fancy-ui-svelte";
	import Plus from "@lucide/svelte/icons/plus";
	import ArrowRight from "@lucide/svelte/icons/arrow-right";
</script>

{#snippet plusIcon()}
	<Plus class="size-4" />
{/snippet}

{#snippet arrowIcon()}
	<ArrowRight class="size-4" />
{/snippet}

<div class="flex flex-wrap items-center gap-3">
	<Button sound iconStart={plusIcon}>New project</Button>
	<Button sound variant="outline" iconEnd={arrowIcon}>Continue</Button>
</div>
```

### As Link

href renders an anchor instead of a button.

```svelte
<script lang="ts">
	import { Button } from "fancy-ui-svelte";
</script>

<div class="flex flex-wrap items-center gap-3">
	<Button sound href="/docs/components/button" variant="outline">Read the docs</Button>
	<Button sound href="https://svelte.dev" target="_blank" variant="ghost">
		Svelte docs (opens in a new tab)
	</Button>
</div>
```

### Disabled State

Disabled on the button vs. the anchor — two different mechanisms.

```svelte
<script lang="ts">
	import { Button } from "fancy-ui-svelte";
</script>

<div class="flex flex-col gap-4">
	<div class="flex flex-wrap items-center gap-3">
		<Button disabled>Disabled</Button>
		<Button disabled variant="secondary">Disabled</Button>
		<Button disabled variant="outline">Disabled</Button>
		<Button disabled href="https://example.com">Disabled link</Button>
	</div>

	<p class="text-muted-foreground max-w-md text-sm leading-relaxed">
		The button and the link are inert for different reasons. The button gets the real
		<code class="text-xs">disabled</code> attribute; the link has no such state to lean on, so it
		drops <code class="text-xs">href</code> entirely rather than relying on a click handler alone to stop
		the navigation.
	</p>
</div>
```
