Button
Triggers an action or, with asChild, a navigation. A pill by default, ink at rest, tints on hover and a 150ms press under motion-safe. Source: src/components/ui/button.tsx.
Variants
Nine looks. Ink is the default; tangerine is spent once per screen, on the action the screen exists for.
When to use each
| Example | Prop | Usage |
|---|---|---|
| Ink. The default action of a view or dialog. | ||
| Tangerine. The one call to action on a screen, at most. | ||
| Filled neutral; the quieter partner of the primary action. | ||
| Hairline edge on the canvas: toolbars and card footers. | ||
| No rest fill: row actions, menus, dense toolbars. | ||
| The toolbar pill: tint at rest, text lifts on hover or open. | ||
| A filter chip; aria-pressed turns it ink. | ||
| Red tint for removing something. Confirm it in a dialog. | ||
| Inline text action inside a sentence or a card. |
Sizes
Four text sizes on a 4px step. Default (36px) for page actions, sm (32px) in toolbars and cards, xs (28px) inside dense rows, lg (40px) for a form's submit.
Pill
The composer's size. Same 32px as sm, but it keeps 12px of padding when an icon leads, so a row of toolbar pills has even sides.
Icon sizes
Square buttons for a lone icon. Each one needs an aria-label; the icon itself is aria-hidden. The 16px icon shrinks to 14px only at icon-2xs.
Prefix and suffix
Put the icon inside as a direct child, before or after the label. The button sizes it to 16px and pulls the padding on that side in by 4px.
Shapes
Every control is a pill. Soft (6px corners) is the escape for stacked, full-width forms such as sign-in and settings, where a wide pill reads as a lozenge.
States
State comes from ARIA, not from extra props: the button styles aria-expanded, aria-pressed and aria-invalid, so a Radix trigger or a toggle gets the right look for free.
Disabled
Open, pressed and invalid
As link
When the button goes somewhere, render the link with asChild. It keeps every class and state, and the page gets a real anchor: middle-click, copy link and prefetch all work.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "default" | "brand" | "secondary" | "outline" | "ghost" | "tint" | "chip" | "destructive" | "link" | "default" | The look. Mirrored as data-variant. |
| size | "xs" | "sm" | "default" | "lg" | "pill" | "icon-2xs" | "icon-xs" | "icon-sm" | "icon" | "icon-lg" | "none" | "default" | Height and padding. none drops height, padding and gap for a caller that owns the box. Mirrored as data-size. |
| shape | "pill" | "soft" | "pill" | soft swaps rounded-full for rounded-sm. Mirrored as data-shape. |
| asChild | boolean | false | Render the single child (a Link, a label) with the button's classes, via Radix Slot. |
| ...props | ComponentProps<"button"> | — | Native attributes pass through. className merges last, so it wins. |
Best practices
When to use
- One brand button per screen. The composer's send, the trace's play: everything else is ink, tint or ghost.
- A Button that navigates is asChild around next/link, never an onClick that pushes a route.
- Pages never render a raw button element outside ui/; npm test fails on one.
- Need a size the scale lacks? Use size="none" and own the box, then raise it in docs/components.md §5.
Behavior
- Labels are verbs: Save, Send, New agent. Not OK, not Submit.
- Keep the weight constant across states; hover changes fill and colour only, so nothing reflows.
- Overriding the transition list? Carry scale through, or the press feedback snaps.
- Destructive actions confirm in a dialog; the destructive variant alone is not a safeguard.
Accessibility
- Icon-only buttons take an aria-label; the Tabler icon inside takes aria-hidden.
- Pair an icon-only button with a Tooltip that repeats the label for pointer users.
- Use aria-pressed for toggles and aria-expanded for menu triggers; the styles follow.
- The focus ring is built in (2px, offset). Don't remove the outline in className.
Touch
- icon-2xs and icon-xs are under 44px: keep them to pointer-dense surfaces such as message actions.
- On a phone-first form use lg (40px) at full width with shape="soft".