Multi-state button
A button whose width, glyph and label morph as it moves between states.
API reference
Components
<MultiStateButton />
A pill button whose content crossfades and whose width morphs on the compositor as its consumer-owned state changes, with an optional shake/pop when the state settles. Composes the per-state glyph + label the consumer supplies; times everything from the Motion UI theme.
state
The current state key - the consumer owns and advances the state machine. Each change keys the content crossfade, morphs the pill width and (if feedback is set) fires the settle feedback. Any string works: an "idle" / "loading" / "success" submit flow, a four-step cycle, etc.
children
The visible label for the current state - the text that morphs.
icon?
An optional leading glyph for the current state, rendered before the label (decorative, wrapped aria-hidden). Size and colour it from the glyph itself - typically an inlined spinner/check/cross per integration. Omit for a text-only state (e.g. an idle "Subscribe").
surfaceClassName?
Surface-colour classes for the CURRENT state, merged onto the morphing pill (e.g. "bg-primary text-primary-foreground" for a neutral state, "bg-success text-success-foreground" for a completed one). Changing this with state cross-fades the colour on the theme's snap curve. Default "bg-primary text-primary-foreground".
pillClassName?
Constant pill styling applied in EVERY state - shape, padding and text size - kept separate from surfaceClassName so the per-state colour is the only thing that changes. Default "rounded-md px-5 py-3 text-sm font-medium".
feedback?
Imperative feedback fired when state settles: "shake" for a failed action, "pop" for a completed one, "none" (default) for neither. Derive it from the current state (e.g. state === "failed" ? "shake" : state === "ready" ? "pop" : "none"). Never plays under reduced motion.
widthMorph?
Whether the pill animates its width on the compositor (via Motion layout projection) as the content changes. Default true; set false for a fixed-width button whose content swaps in place.
announce?
Accessible status text for the current state, announced through an internal aria-live="polite" region rendered alongside the button. Omit to supply your own live region (a form's shared status line, say). When set, also pass aria-label so the announcement does not leak into the button's accessible name.
"aria-label"?
Accessible name for the button. Set this when the visible label alone does not describe the action in every state (e.g. a submit whose label is just a glyph), so a screen-reader user hears the current action.
type?
Button type. "button" (default) for a standalone action; "submit" for a form-embedded one whose form owns the submit.
disabled?
Disables the button (e.g. while a request is in flight). Adds pointer-events-none; add your own dimming via className if wanted.
onClick?
Click handler - advance your state machine, or leave undefined and let a type="submit" button's form handle it.
className?
Merged onto the button root (the focus target).
Featured in
See the multi-state button composed into full sections.

