# FollowButton

> A follow toggle that shows the result at once, then commits when the server agrees or rolls back and says so, with the width held steady across every label.

- Package: `fancy-ui-svelte` (Svelte 5 + Tailwind CSS v4)
- Category: Actions
- Tags: follow, subscribe, toggle, async, optimistic, button
- Dependencies: button, status-morph
- Types: `FollowButtonProps`, `FollowButtonAttributes`, `FollowLabels`
- Docs: https://fancy-ui.rama.app/docs/components/follow-button
- Source: https://github.com/RamaHerbin/fancy-ui/tree/main/src/lib/fancy-ui/follow-button

## Import

```svelte
<script lang="ts">
  import { FollowButton } 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 { FollowButton } from 'fancy-ui-react';` (`class` becomes `className`, snippets become `ReactNode`, DOM callbacks are camel-cased; exact names on https://fancy-ui.rama.app/docs/components/react/follow-button.md).

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `following` | `boolean` | `false` | Whether the viewer follows (bindable). The committed value is written back when onToggle resolves, never before; a write from the parent while an attempt is pending wins and drops that attempt |
| `onToggle` | `(next: boolean) => void \| Promise<unknown>` | — | Called with the value the click asks for. Return nothing to commit at once, or a promise: resolve commits, reject (or throw) rolls back, shows the error and announces it. Clicking again retries |
| `onFollowingChange` | `(following: boolean) => void` | — | Called with the committed value, once per successful attempt; not called for a rolled-back or superseded one |
| `label` | `string` | — | Stable accessible name, e.g. "Follow Rama"; it never changes with the visible text. Defaults to the idle label |
| `labels` | `{ follow?: string; following?: string; unfollow?: string }` | `{ follow: "Follow", following: "Following", unfollow: "Unfollow" }` | Visible text per state. The unfollow text is previewed on hover or keyboard focus of a fine pointer, never on touch |
| `errorLabel` | `string` | `"Try again"` | Visible text and assertive announcement after a failed attempt |
| `resetMs` | `number` | `2000` | How long the error state holds before reverting, in milliseconds |
| `variant` | `ButtonVariant` | — | Button variant. Left unset, the idle state is "primary" and the following state "outline" |
| `size` | `ButtonSize` | `"md"` | Passed straight through to the underlying Button |
| `disabled` | `boolean` | `false` | Disables the button and blocks the toggle |
| `iconOnly` | `boolean` | `false` | Drops the visible text, keeping the accessible name |
| `onclick` | `(event: MouseEvent) => void` | — | Click handler, called right after the toggle starts; never called while disabled or pending |
| `class` | `string` | — | Additional CSS classes |
| `ref` | `HTMLButtonElement \| HTMLAnchorElement \| null` | `null` | Bindable element reference, matching Button's own ref type |
| `sound` | `boolean` | `false` | Plays toggle-on or toggle-off on a committed change and error on a failed one, with the error vibration, once the user has enabled sound |

## Examples

### Basic Usage

A bindable toggle that commits at once when nothing needs to be awaited.

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

	let following = $state(false);
</script>

<FollowButton sound bind:following label="Follow Rama" />
```

### Async Server

A fake 800 ms server and a switch that makes the next request fail: watch the optimistic flip, the rollback, and the retry.

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

	let following = $state(false);
	let failNext = $state(false);

	// A fake server: every request takes 800 ms. With the switch on, the next one
	// is refused (and the switch turns itself off), so the rollback and the retry
	// can both be watched.
	async function onToggle(_next: boolean) {
		// Read and consume the flag when the request STARTS: a request sent under
		// "Fail next request" fails even if the switch is turned off meanwhile.
		const refuse = failNext;
		failNext = false;
		await new Promise((resolve) => setTimeout(resolve, 800));
		if (refuse) throw new Error("The server said no");
	}
</script>

<div class="flex flex-col items-center gap-4">
	<FollowButton sound bind:following {onToggle} label="Follow Rama" />
	<Switch bind:checked={failNext}>Fail next request</Switch>
</div>
```
