Status morph
FreeA status that changes in place: the pill eases to its new width while icon and colour cross over, and the change is announced.
pnpm dlx shadcn@latest add https://caliperui.site/r/status-morph.jsonDeploy #418Queued
When to use #
Use it for
- Deploys, jobs, payments and anything that moves through states.
- Rows that update while people watch.
Skip it when
- Static labels; use a badge.
Installation #
pnpm dlx shadcn@latest add https://caliperui.site/r/status-morph.json
Adds its 2 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above.
import { useState } from "react" import { Button } from "@/components/ui/button" import { StatusMorph } from "@/components/ui/status-morph" const deployStates = { queued: { label: "Queued", tone: "neutral" }, building: { label: "Building", tone: "progress" }, checks: { label: "Running checks", tone: "progress" }, live: { label: "Live", tone: "success" }, failed: { label: "Failed: tests", tone: "error" }, } as const; type DeployState = keyof typeof deployStates; export function StatusMorphDemo() { const order: DeployState[] = ["queued", "building", "checks", "live"]; const [status, setStatus] = useState<DeployState>("queued"); const next = () => setStatus((current) => order[(order.indexOf(current) + 1) % order.length] ?? "queued"); return ( <div className="grid justify-items-center gap-4"> <p className="flex items-center gap-3 text-sm"><span className="text-muted-foreground">Deploy #418</span><StatusMorph status={status} states={deployStates} /></p> <div className="flex gap-2"> <Button size="sm" variant="secondary" onClick={next}>Advance</Button> <Button size="sm" variant="ghost" onClick={() => setStatus("failed")}>Fail it</Button> </div> </div> ); }
API reference #
StatusMorph
component
Also accepts the native props of its root element and a ref.
Accessibility #
It exposes status semantics, with aria-live and aria-hidden kept in step with state. Changes that matter are announced through a live region.
No custom key handling; Tab and the native activation keys do the work.
Motion #
It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.