Button

Starts an action.

Styles

  • primarydefault

  • secondary

  • ghost

  • destructive

Sizes

  • sm

  • defaultdefault

  • lg

  • icon-sm

  • icon

  • icon-lg

States

  • rest

  • hover

  • held

  • focus-visible

  • disabled

  • pending

Usage

When to use it
Use
The person does something now: run, replay, deploy, save, cancel a job.
Use
A frame, footer, dialog or toolbar ends in one action, with a way out beside it.
Use
A row repeats one quiet action on every item (ghost, icon sizes).
Not for
The control moves to another page. Use a link; a Button acts on this page.
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.

Rules

10 rules
Must
Give a frame, footer, dialog or any group of actions at most one primary Button: variant="primary" (the yellow slab) or "destructive". Make every other action beside it variant="secondary" or "ghost". Because the slab is the one solid yellow on the screen and marks the next step, so two of them split the eye; primary is the default variant, so two bare Buttons are two slabs.
Must
Lead with variant="destructive" only when the action loses data or work that cannot come back: drop, purge, revoke, cancel a running job. Because red is Sodium's failure colour, so on Save or Retry it claims something already went wrong.
Never
Paint a secondary or ghost Button's label, icon or edge in yellow to make it stand out. Because yellow on white is 1.4:1 and vanishes, and in dark a yellow edge reads as a second primary.
Must
Write the label in sentence case as a verb and its object; the button sets the capitals. Because the label role uppercases it on screen, while a screen reader reads the source, and capitals typed in are spelled out letter by letter.
Must
Pass pending as a boolean from the first render; the mark stands in the label's place, or add pendingLabel for a verb. Never swap the children while an action runs. Because Button measures the pending layer only when pending is defined, and swapped children change the width and the accessible name mid-press.
Must
Give every icon size an aria-label. Because the button holds only a glyph, so a screen reader announces "button" and the development 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
Pass fills, edges, corners, heights or type in className; pass only placement, such as a margin or a width. Because a hand-set fill drops the hover, held and disabled steps and the light from below, every corner in Sodium is square, and a fixed height clips the label at 200% zoom.
Never
Wrap a Button in an <a>; render it as the anchor with render and nativeButton={false}. Because a button inside a link is invalid HTML, takes two tab stops and is announced as both.
Should
Use size="sm" in a 40px row or a panel head, the default in forms and toolbars, and lg only for a page's one main action. Because a 26px button sits 7px inside a row's rules, where a 32px one crowds them.

State order

6 states, highest first
1
pending
2
disabled
3
held
4
focus-visible
5
hover
6
rest

Accessibility

6 checks
Focus is a 2px square outline 2px out in --focus, dark gold in light and yellow in dark, on every variant.
Labels where they sit: ink on the slab 12.8:1, yellow on the dark tint 9.0:1, the failure word on its tint 5.7:1 and 5.6:1, text-1 on the page 18.3:1 and 16.6:1, ghost text-2 7.8:1 and 10.2:1.
The secondary's edge is --line, the rule colour, 1.3:1 on the page; its text-1 label names it, not the edge.
The capitals come from CSS, so a screen reader reads the sentence-case source as words.
Name icon sizes with aria-label; the glyph is aria-hidden. Development builds log a button with no name.
Pending sets aria-busy, keeps focus and keeps the idle name; the pending mark and verb are aria-hidden.

Keyboard

2 keys
Enter
Presses the button.
Space
Presses the button on release.

Edge cases

10 edge cases
Long label
Never wraps; a label wider than its container ends in an ellipsis and stays the accessible name.
Row narrower than its buttons
Buttons never shrink. Let the row wrap with flex-wrap.
Zoom 200%
The height grows with the label past its token.
Slow request
The box keeps its width, focus stays, the pending mark steps and repeat presses are blocked.
Failed request
Clear pending and the button returns to rest with its name; say what failed in a status region.
Hover and held
Primary and secondary are lit from below, and hover lightens the fill, the edge and the light and adds a 1px halo. Held settles them back to rest with the light out and the halo kept; ghost and destructive step a wash.
Disabled
Every variant drops to the well with a rule edge and the disabled label, unlit, because a control that cannot be pressed should not look like the one to press.
Dark theme
The slab turns into the dark yellow tint with a 1px yellow line, a yellow label and a faint yellow light; the secondary sits on a body a step off the near-black page with a fainter light.
Forced colors
The light from below and the halo are dropped; the slab and the destructive keep a ButtonText edge; the focus outline stays.
Reduced motion
The pending mark stands still with its first cell lit; the colour and light still change, at once.

API

8 props
variant
'primary' | 'destructive' | 'secondary' | 'ghost' default 'primary'
size
'sm' | 'default' | 'lg' | 'icon-sm' | 'icon' | 'icon-lg' default 'default'
pending
boolean
pendingLabel
ReactNode
disabled
boolean default false
focusableWhenDisabled
boolean default false
render
ReactElement
className
string

Tokens

44 tokens
--primary-bg
#ffd400
--primary-text
#161512
--primary-edge
#d9b400
--primary-hover
#ffdf3d
--primary-edge-hover
#f0cf2c
--primary-pressed
#ffd400
--primary-light
rgb(255 255 255 / 0.55)
--primary-light-hover
rgb(255 255 255 / 0.85)
--secondary-bg
#f8f7f3
--secondary-hover
#fefefd
--secondary-pressed
#f8f7f3
--secondary-edge
var(--line)
--secondary-edge-hover
#ecebe5
--secondary-light
rgb(255 255 255 / 0.95)
--secondary-light-hover
rgb(255 255 255 / 1)
--primary-ring
#fff9df
--secondary-ring
#f4f3ee
--fail
#b3261e
--fail-text
#b3261e
--fail-tint
#fdecea
--line
#e2dfd5
--hover
rgb(22 21 18 / 0.04)
--pressed
rgb(22 21 18 / 0.08)
--well
#f4f3ee
--text-1
#161512
--text-2
#55524a
--text-disabled
#a8a499
--control-sm
26px
--control
32px
--control-lg
36px
--icon
16px
--type-label
400 13px/16px var(--font-mono)
--type-label-sm
400 12px/16px var(--font-mono)
--type-label-lg
400 14px/20px var(--font-mono)
--focus
#7a5d00
--focus-width
2px
--focus-offset
2px
--pending-cell
3px
--dur-press
80ms
--dur-light
240ms
--dur-glow
320ms
--dur-pending
800ms
--ease-out
cubic-bezier(0.2, 0.8, 0.2, 1)
--ease-soft
cubic-bezier(0.22, 0.8, 0.24, 1)

Install

For your coding agent

GitHub

Please star me pookie

198

A new design system every week

Leave your email before you copy the prompt.