Skip to content
HyperDesign System

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

Button variants
ExamplePropUsage
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.

xs · 28
sm · 32
default · 36
lg · 40

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.

sm
pill

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.

icon-2xs · 22
icon-xs · 24
icon-sm · 32
icon · 36
icon-lg · 40

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

aria-expanded
aria-pressed
aria-invalid

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

Button props
PropTypeDefaultDescription
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.
asChildbooleanfalseRender the single child (a Link, a label) with the button's classes, via Radix Slot.
...propsComponentProps<"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".