# 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-react` (React 18/19 + Tailwind CSS v4)
- Category: Actions
- Tags: follow, subscribe, toggle, async, optimistic, button
- Dependencies: button, status-morph
- Types: `FollowButtonProps`, `FollowLabels`
- Docs: https://fancy-ui.rama.app/docs/components/follow-button
- Source: https://github.com/RamaHerbin/fancy-ui/tree/main/react/src/components/follow-button

## Import

```tsx
import { FollowButton } from 'fancy-ui-react';
```

Setup (once per project): `npm install fancy-ui-react`, then put these lines in your main CSS file (the stylesheet import goes right after `@import "tailwindcss";`):

```css
@import "tailwindcss";
@import "fancy-ui-react/tailwind.css";
```

and import the component styles once in the app entry: `import "fancy-ui-react/styles.css";`. Full guide: https://fancy-ui.rama.app/llms.txt

The names below are the ones to write in React; when one differs from the Svelte name, the Svelte name follows in parentheses. `class` becomes `className`, a callback takes the name the React port declares (`onclick` becomes `onClick`), snippets become `ReactNode` props (render functions when they take arguments) and the default snippet is `children`. Divergences: https://github.com/RamaHerbin/fancy-ui/tree/main/react#divergences-from-the-svelte-api

## 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` (Svelte `onclick`) | `(event: MouseEvent) => void` | — | Click handler, called right after the toggle starts; never called while disabled or pending |
| `className` (Svelte `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 |

## Example

```tsx
import { useState } from "react";
import { FollowButton, Switch } from "fancy-ui-react";

export function FollowButtonExample() {
	const [following, setFollowing] = useState(false);
	const [failNext, setFailNext] = useState(false);

	// A fake server: every request takes 800 ms. With the switch on, the next
	// one is refused, so the rollback and the retry can both be watched.
	async function onToggle() {
		await new Promise((resolve) => setTimeout(resolve, 800));
		if (failNext) {
			setFailNext(false);
			throw new Error("The server said no");
		}
	}

	return (
		<div className="flex flex-col items-center gap-4">
			<FollowButton
				sound
				following={following}
				onFollowingChange={setFollowing}
				onToggle={onToggle}
				label="Follow Rama"
			/>
			<Switch checked={failNext} onCheckedChange={setFailNext}>
				Fail next request
			</Switch>
		</div>
	);
}
```
