Skip to content

Button

A <Button> is the primary primitive for any action that does one thing on click. Six visual variants, four height sizes, plus icon-only sizes. Composes Tooltip and disabledReason natively so disabled buttons can explain themselves.

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

When to use

  • Every clickable thing that performs an action on the current page (Save, Delete, Create, Cancel).
  • Form submission. Cancel actions in dialogs.
  • Toolbar actions (use size="sm" or size="icon-sm").

Don’t use for navigation. A clickable thing that takes the user somewhere else is a link — use an <a> (or <Link>). Nav-as-button is bad for keyboard, screen readers, and middle-click “open in new tab”.

Variants

The variant carries the meaning. Don’t pick a variant by aesthetics.

<Button>Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="link">Link</Button>
VariantUse
defaultThe single most-important action on a screen. One per surface.
secondaryThe next-most-important action. Pair with default for two-action toolbars.
outlineNeutral actions (“Cancel”, “Back”). Pair with default in dialogs.
ghostToolbar / inline actions where chrome is expensive.
destructiveIrreversible operations only. Always paired with a confirmation.
linkInline action inside body text. Don’t use as a primary CTA.

Sizes

<Button size="lg">Large</Button>
<Button size="default">Default</Button>
<Button size="sm">Small</Button>
<Button size="xs">Extra small</Button>

Icon-only

Use icon-only when the icon alone communicates the action (Tabler icons are unambiguous for most CRUD verbs). Always supply tooltip so the action has a name.

<Button size="icon" tooltip="Add row"><IconPlus /></Button>
<Button size="icon-sm" variant="ghost" tooltip="Open"><IconArrowRight /></Button>
<Button size="icon" variant="destructive" tooltip="Delete"><IconTrash /></Button>

Icon + label

Leading icons reinforce intent; trailing icons indicate flow. Don’t put both.

<Button><IconPlus />Create deal</Button>
<Button variant="outline">Continue<IconArrowRight /></Button>

Disabled with a reason

Disabled buttons fail an unstated test. Telling the user why turns a dead-end into guidance — set disabledReason and the tooltip surfaces only while disabled.

<Button disabled disabledReason="Fill in the agent name first.">Save</Button>

Composing with tooltip

When the visible label doesn’t convey full intent (icon-only, abbreviated label, advanced action), pass tooltip. Don’t restate the visible label — that’s noise.

<Button variant="ghost" size="sm" tooltip="Re-run with same input">Retry</Button>

API

Prop Type Default Description
variant "default" | "secondary" | "outline" | "ghost" | "destructive" | "link" "default" Visual + semantic variant.
size "default" | "lg" | "sm" | "xs" | "icon" | "icon-lg" | "icon-sm" | "icon-xs" "default" Height + padding scale. Icon sizes are square.
asChild boolean false Render the button styling onto the child element (Radix Slot). Use when wrapping an <a> for true nav links.
tooltip ReactNode Hover tooltip. Use only when the visible label is insufficient.
disabledReason ReactNode Tooltip shown only while disabled, explaining why. Strongly recommended for any disabled state a user might want to un-disable.
tooltipSide "top" | "right" | "bottom" | "left" "top" Tooltip placement.
disabled boolean false Native disabled. When set with disabledReason, the button is wrapped to receive hover.
onClick (e: MouseEvent) => void Native click handler.
...rest ButtonHTMLAttributes All native <button> props are forwarded.

Design guidelines

✓ Do

  • Use one default-variant button per surface — it answers "what should I do here".
  • Lead with the verb: "Create deal", "Save changes", "Delete agent". Sentence case.
  • Pair destructive actions with a ConfirmDialog. Never one-click delete.
  • Set disabledReason on every disabled button the user could un-disable.

✗ Don't

  • Use a button for navigation — that is a link.
  • Stack two default-variant buttons next to each other. Pick one primary, one secondary.
  • Set a tooltip that just restates the visible label.
  • Use destructive-variant for routine actions to "draw attention". The system has only one red.

Accessibility

  • Native <button> semantics — role="button" is implicit, no need to set.
  • Keyboard: Tab to focus, Space or Enter to activate. Browser-default.
  • Focus ring: 3px brand-tinted ring via focus-visible:. Visible on keyboard focus, hidden on click — not by us, by :focus-visible the spec.
  • Disabled buttons set aria-disabled implicitly via the disabled attribute.
  • Icon-only buttons must set tooltip (which doubles as aria-label via the tooltip’s text content).
  • The disabledReason wrapper sets aria-label on the wrapping span when the reason is a string, so screen readers announce the reason.
  • ActionButton — Button with mutation state (idle / pending / success / error). Use whenever the click triggers a fetch.
  • ConfirmDialog — Required pairing for destructive actions.
  • Tooltip — The underlying primitive. Use directly when the trigger isn’t a button.

▶ Open Button stories in Storybook