# 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-vue` (Vue 3.5+ + 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/vue/src/components/follow-button

## Import

```vue
<script setup lang="ts">
import { FollowButton } from 'fancy-ui-vue';
</script>
```

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

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

Full guide: https://fancy-ui.rama.app/llms.txt

The names below are the ones to write in a Vue template; when one differs from the Svelte name, the Svelte name follows in parentheses. Snippets become named slots (`#default` is the default snippet), a prop the Vue port declares as a model is a `v-model:<name>` (except `ref`, exposed via `defineExpose({ ref })`: put a template ref on the component and read `.ref`) and callbacks are listeners (`onclick` is `@click`). Every component sets `inheritAttrs: false`: `class` is a declared prop, merged where the Svelte component applies it (not always the root element), and only when the props table below lists it; other attributes do not fall through unless the package README lists the component as forwarding native attributes. Divergences: https://github.com/RamaHerbin/fancy-ui/tree/main/vue#divergences-from-the-svelte-api

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `v-model:following` (Svelte `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 |
| `@toggle` (Svelte `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 |
| `@following-change` (Svelte `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 |
| `@click` (Svelte `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 |

## Example

```vue
<script setup lang="ts">
import { ref } from "vue";
import { FollowButton, Switch } from "fancy-ui-vue";

const following = ref(false);
const failNext = ref(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() {
	// 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.value;
	failNext.value = false;
	await new Promise((resolve) => setTimeout(resolve, 800));
	if (refuse) throw new Error("The server said no");
}
</script>

<template>
	<div class="flex flex-col items-center gap-4">
		<FollowButton sound v-model:following="following" :on-toggle="onToggle" label="Follow Rama" />
		<Switch v-model:checked="failNext">Fail next request</Switch>
	</div>
</template>
```
