Feedback Fancy Stable
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
Preview
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>Installation
pnpm add fancy-ui-svelte
import { StatusMorph } from 'fancy-ui-svelte'
Usage
svelte
<script lang="ts">
import { StatusMorph } from 'fancy-ui-svelte';
</script>
<StatusMorph />Examples
Basic Usage
svelte
<script lang="ts">
import { Button } from "$lib/fancy-ui/button";
import { StatusMorph } from "$lib/fancy-ui/status-morph";
import { Switch } from "$lib/fancy-ui/switch";
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 "$lib/fancy-ui/status-morph";
import { Button } from "$lib/fancy-ui/button";
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 "$lib/fancy-ui/status-morph";
</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>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 |
Slots
| Slot | 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 |
Links
Related components
An
AnimatedTooltip
Avatar row with animated tooltips that follow mouse movement
Feedback Stable
Sk
Skeleton
Placeholder bones — a block, one or more text lines, or a circular avatar — with a phase-synced shimmer sweep or opacity pulse, that swaps to real content once loading finishes while keeping aria-busy semantics correct throughout
Feedback Stable