# StatusMorph

> A 1em SVG status icon that morphs between idle, loading, success, and error, closing its spinning ring into a drawn check or cross with a portalled live region announcing each state.

- Package: `fancy-ui-svelte` (Svelte 5 + Tailwind CSS v4)
- Category: Feedback
- Tags: status, icon, loading, success, error, morph, micro-interaction
- Types: `StatusMorphProps`
- Docs: https://fancy-ui.rama.app/docs/components/status-morph
- Source: https://github.com/RamaHerbin/fancy-ui/tree/main/src/lib/fancy-ui/status-morph

## Import

```svelte
<script lang="ts">
  import { StatusMorph } 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 { StatusMorph } 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/status-morph.md).

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `state` | `"idle" \| "loading" \| "success" \| "error"` | `"idle"` | Current state, two-way bound. The component only ever writes it back to "idle" itself, when resetAfter fires — every other write is the caller's, and is always honoured |
| `resetAfter` | `number` | `1800` | Milliseconds until an automatic reset to "idle" after "success" or "error". 0 disables the timer entirely (manual reset only) |
| `labels` | `{ loading?: string; success?: string; error?: string }` | `{ loading: "Loading", success: "Done", error: "Failed" }` | Live-region text per state; unset keys fall back to the defaults |
| `tone` | `"current" \| "semantic"` | `"current"` | "current" paints every glyph in currentColor; "semantic" reads the shared --ft-status-running/-done/-error vocabulary instead |
| `haptic` | `boolean` | `false` | Best-effort tactile feedback (the success/error haptic patterns) on entering those states; silently a no-op wherever the Vibration API is unsupported or refused |
| `class` | `string` | — | Additional CSS classes |
| `ref` | `HTMLSpanElement \| null` | `null` | Bindable reference to the root element |

## Snippets

| Snippet | Description |
| --- | --- |
| `idle` | Custom idle content, rendered in the same calc(1em + 1px) footprint instead of the default transparent scaffold, so swapping to it never shifts layout |

## Examples

### Basic Usage

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

	type Status = "idle" | "loading" | "success" | "error";

	let status = $state<Status>("idle");
	let failNext = $state(false);

	// Composition recipe from the README, frozen: Button's own spinner covers the
	// entire loading phase (iconStart never mounts until loading is already false),
	// so StatusMorph only ever plays its check-pop / error-shake here, not the full
	// ring→check morph — that's reserved for standalone usage (see the Standalone
	// example).
	async function save() {
		status = "loading";
		await new Promise((resolve) => setTimeout(resolve, 1200));
		status = failNext ? "error" : "success";
	}
</script>

{#snippet icon()}
	<StatusMorph bind:state={status} />
{/snippet}

<div class="flex w-full flex-col items-center gap-6 p-6">
	<Button loading={status === "loading"} iconStart={icon} onclick={save}>Save changes</Button>

	<Switch bind:checked={failNext}>Fail next save</Switch>
</div>
```

### Standalone

The full ring → check/cross morph, outside the Button composition.

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

	type Status = "idle" | "loading" | "success" | "error";

	let state = $state<Status>("idle");
</script>

<div class="flex w-full flex-col items-center gap-6 p-6">
	<!-- Standalone usage — nothing hides StatusMorph's loading phase the way
	     Button's own spinner does, so the full ring → check/cross morph plays
	     every time, not just the settle. -->
	<div class="text-foreground flex items-center gap-3 text-3xl">
		<StatusMorph bind:state />
		<span class="text-muted-foreground text-sm">Sync status</span>
	</div>

	<div class="flex flex-wrap justify-center gap-2">
		<Button size="sm" variant="outline" onclick={() => (state = "loading")}>Loading</Button>
		<Button size="sm" variant="outline" onclick={() => (state = "success")}>Success</Button>
		<Button size="sm" variant="outline" onclick={() => (state = "error")}>Error</Button>
		<Button size="sm" variant="ghost" onclick={() => (state = "idle")}>Reset</Button>
	</div>
</div>
```

### Semantic Tone

tone="semantic" reads the shared status colors.

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

<div class="flex w-full flex-col items-center gap-6 p-6">
	<!-- tone="semantic" swaps currentColor for the shared AI-family
	     --ft-status-running/-done/-error vocabulary — the ring track itself stays
	     neutral in both tones. Three static states side by side, so the three
	     colors are visible at once instead of only ever one at a time;
	     resetAfter={0} keeps the settled states from auto-resetting to idle. -->
	<div class="flex items-center gap-8 text-3xl">
		<div class="flex flex-col items-center gap-2">
			<StatusMorph state="loading" tone="semantic" />
			<span class="text-muted-foreground text-xs">loading</span>
		</div>
		<div class="flex flex-col items-center gap-2">
			<StatusMorph state="success" tone="semantic" resetAfter={0} />
			<span class="text-muted-foreground text-xs">success</span>
		</div>
		<div class="flex flex-col items-center gap-2">
			<StatusMorph state="error" tone="semantic" resetAfter={0} />
			<span class="text-muted-foreground text-xs">error</span>
		</div>
	</div>
</div>
```
