Button

Starts an action, like running an agent or saving a change.

Examples

Busy while publishing

Confirm or go back

Issue refund

Refund $48.00 to Ana Marsh for order 20418.

Filters that stay on

Conversations
12

Styles

  • primarydefault

  • secondary

  • ghost

  • destructive

  • link

Sizes

  • xs

  • sm

  • defaultdefault

  • lg

  • icon-xs

  • icon-sm

  • icon

  • icon-lg

States

  • rest

  • hover

  • focus

  • active

  • disabled

  • pending

Usage

When to use it
Use
The person changes something: create, save, publish, refund, delete.
Use
A row or toolbar repeats one small action on every item.
Use
A single filter stays on until turned off (pressed mode).
Not for
The control turns a setting on or off at once. Use Switch.
Not for
The person picks one of two to five views. Use Segmented control.
Not for
More than three actions compete for one row. Use Dropdown menu.

Rules

12 rules
Must
Use one variant="primary" per view; make every other button secondary or ghost. Because the ink fill marks the next step, so a view has one; primary is the default variant, and two bare Buttons give the view two ink fills and no clear next step.
Must
Show destructive with variant="destructive", a fail edge and fail text; never a red fill. Because a red fill is the loudest shape on the page and outshouts the primary; trouble is written, not painted.
Must
Set pending and pendingLabel; never swap the children while an action runs. Because swapped children change the width and the accessible name mid-press, while pendingLabel shares the idle label's cell.
Must
Pass pending as a boolean from the first render on any button that can pend. Because Button reserves the spinner's room only when pending is defined, so undefined then true makes the box jump.
Must
Give every icon size an aria-label. Because the button holds only a glyph, so a screen reader announces "button" and the dev check logs an error.
Must
Set type="submit" on the Button that submits a form. Because Base UI defaults to type="button", so the press and Enter in a field never submit the form.
Never
Wrap a Button in an <a>. Use Link, or render the Button as an anchor. Because a button inside a link is invalid HTML, takes two tab stops and announces as both a link and a button.
Never
Pass arrow to a variant="link" Button; it acts on this page, and only an external Link carries an arrow. Because an arrow ending link text breaks anti-slop-no-arrow-links and reads as leaving the page.
Must
Pass only placement classes in className, never fills, colors or heights. Because a hand-set fill skips the variant's hover, disabled and forced-colors states, and a fixed height clips text at 200% zoom.
Should
Say why a button is disabled in text beside it, not in a tooltip on it. Because a disabled button takes no hover on touch, so a tooltip on it never shows there.
Should
Label with a verb and its object in sentence case, 40 characters or fewer.
Should
Let a row of buttons wrap with flex-wrap; buttons never shrink. Because Button is shrink-0 and whitespace-nowrap, so a row that cannot wrap overflows a narrow column.

Accessibility

5 checks
Focus shows a 2px ink outline on :focus-visible, on every variant.
Name icon sizes with aria-label; the glyph is aria-hidden.
Pending sets aria-busy and keeps the idle name.
Pressed mode renders aria-pressed; the pressed fill is ink, 17.1:1 for its label.
Report results in a role="status" region, never in the label.

Edge cases

12 edge cases
Long label
Never wraps; the box grows past its container. Keep labels under 40 characters.
Row narrower than its buttons
Buttons never shrink. Wrap the row with flex-wrap.
Right to left
Layout mirrors and the arrow flips.
Zoom 200%
Height grows with the text; the label stays inside.
Slow request
The box keeps its width, focus stays, repeats are blocked.
Failed request
Returns to rest with its name; report the error in a status region.
Empty or unnamed icon button
Development builds log it and set data-unnamed.
Held Enter or double click
Pending blocks repeats; the handler needs no guard.
Inside a form
Presses submit only with type="submit".
Forced colors
Non-link variants draw a ButtonText border; the focus outline survives.
Disabled primary
Fades the whole key with --dim-opacity, since an ink fill has no quieter colour that still reads as a button; the other variants keep their fill and set the label to text-disabled.
Reduced motion
No press scale; the spinner stays as a still ring.

API

11 props
variant
'primary' | 'secondary' | 'ghost' | 'destructive' | 'link' default 'primary'
size
'xs' | 'sm' | 'default' | 'lg' | 'icon-xs' | 'icon-sm' | 'icon' | 'icon-lg' default 'default'
ButtonSize size
'xs' | 'sm' | 'default'
pending
boolean
pendingLabel
ReactNode
arrow
boolean default false
pressed
boolean
kind
'agent' | 'key' | 'run' | 'deploy' | 'model'
focusableWhenDisabled
boolean default false
render
ReactElement
className
string

Tokens

35 tokens
--ink
oklch(var(--ink-c))
--button-primary-hover
oklch(var(--ink-c) / 0.86)
--button-ghost
transparent
--button-ghost-hover
oklch(var(--ink-c) / 0.05)
--line-control
oklch(var(--ink-c) / 0.16)
--line-frame
oklch(var(--ink-c) / var(--line-frame-alpha))
--fail-line
oklch(60% 0.236 26)
--fail-text
oklch(51% 0.180 26)
--fail-wash
oklch(94.5% 0.027 26)
--text-inverse
var(--paper)
--text-disabled
oklch(var(--ink-c) / 0.40)
--dim-opacity
0.4
--surface-inverse
var(--ink)
--surface-pressed
oklch(var(--ink-c) / 0.08)
--elev-control
0 1px 2px -1px oklch(var(--text-on-field-c) / calc(var(--shadow-alpha) * 0.6))
--key-primary-body
linear-gradient(180deg, color-mix(in oklch, var(--ink) 84%, white), color-mix(in oklch, var(--ink) 94%, black))
--key-primary-body-hover
linear-gradient(180deg, color-mix(in oklch, var(--ink) 76%, white), color-mix(in oklch, var(--ink) 90%, white))
--key-primary-edge
inset 0 2px 0 -1px oklch(1 0 0 / 0.2), inset 0 -2px 0 -1px oklch(0 0 0 / 0.3), inset 0 0 0 1px color-mix(in oklch, var(--ink) 88%, black), 0 1px 1px oklch(var(--text-on-field-c) / calc(var(--shadow-alpha) * 1.1)), 0 2px 5px -2px oklch(var(--text-on-field-c) / calc(var(--shadow-alpha) * 1.5))
--key-primary-edge-press
inset 0 0 0 1px color-mix(in oklch, var(--ink) 88%, black), inset 0 1px 2px oklch(0 0 0 / 0.42)
--key-primary-sheen
linear-gradient(90deg, transparent 14%, oklch(1 0 0 / 0.42) 50%, transparent 86%) top / 100% 1px no-repeat, linear-gradient(180deg, oklch(1 0 0 / 0.1), oklch(1 0 0 / 0) 55%)
--key-secondary-body
var(--paper)
--key-secondary-body-hover
oklch(var(--ink-c) / 0.03)
--key-secondary-edge
var(--elev-control)
--key-secondary-edge-press
inset 0 1px 2px oklch(var(--ink-c) / 0.12)
--control-xs
24px
--control-sm
28px
--control
32px
--control-lg
44px
--radius-soft
6px
--line-focus
2px
--border-focus
var(--ink)
--focus-offset
2px
--dur-fast
120ms
--dur-press
60ms
--ease-step
cubic-bezier(0.2, 0, 0, 1)

Install

For your coding agent

GitHub

Please star me pookie

141

A new design system every week

Leave your email before you copy the prompt.