Skip to content

ActionButton

<ActionButton> is <Button> plus an in-button mutation state machine. Pass a mutation prop with { status, error }; the button shows a spinner while pending, ✓ on success (auto-restores), and surfaces the error inline / via tooltip on failure.

import from "@na/ui/components/ActionButton" ▶ Open in Storybook packages/ui/src/components/ActionButton.tsx

When to use

  • Any button that triggers a fetch / mutation. Save, Publish, Delete (paired with Confirm).
  • Submit buttons in forms — mutation comes from your hook (react-query, SWR, …).

Don’t use ActionButton for: navigation links (use <a> or <Button asChild>), pure UI toggles (Button is enough).

States

<ActionButton mutation={mutationLike}>Save</ActionButton>

mutationLike is { status: 'idle' | 'pending' | 'success' | 'error', error: unknown } — works directly with react-query / SWR / TanStack Query mutation results.

Round trip

const [m, setM] = useState({ status: 'idle', error: null });
<ActionButton
mutation={m}
onClick={async () => {
setM({ status: 'pending', error: null });
try {
await api.save();
setM({ status: 'success', error: null });
} catch (e) {
setM({ status: 'error', error: e });
}
}}
>
Save
</ActionButton>

API

Prop Type Default Description
mutation * { status, error } `status` is "idle" | "pending" | "success" | "error". `error` is the thrown value when status="error".
idleIcon ReactNode Leading icon while idle.
pendingText ReactNode Label swapped in while pending. Falls back to children.
successText ReactNode Label swapped in while success.
successTimeout number 1500 Ms before success state restores to idle. 0 = sticky until next click.
errorPlacement "tooltip" | "below" | "below-absolute" | "none" "tooltip" Where the error message surfaces.
formatError (err: unknown) => string Override the default error-message formatter (extractErrorMessage).
onError (err: unknown) => void Fires once on transition into error. Useful for toast-lifting.
tooltip ReactNode Hover tooltip while idle. Overridden by error message in tooltip placement.
disabledReason ReactNode Tooltip shown only while disabled.
...rest ButtonProps All Button props (variant, size, asChild, …).

Design guidelines

✓ Do

  • Pass the mutation result directly. Don't mirror state into your own component.
  • Use errorPlacement="tooltip" for icon-only buttons; "below" inside forms next to FormError.
  • Pair with onError to lift submission errors into a Sonner toast when the user has navigated away.

✗ Don't

  • Set disabled while pending — ActionButton already handles this.
  • Show success indefinitely (successTimeout=0) for routine saves. Stickiness reads as "this didn't work".
  • Use ActionButton for non-async clicks. Plain Button is faster.

Accessibility

  • Inherits <Button> semantics — proper focus ring, keyboard activation.
  • aria-busy="true" while pending; screen readers announce loading.
  • Error in tooltip mode pairs with aria-describedby referring to the tooltip content.
  • Button — Underlying primitive.
  • MutationStatus — Same state machine, no button — for inline status next to other controls.
  • Form — Pair the submit button with FormError above it.

▶ Open ActionButton stories in Storybook