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'pendingbooleanpendingLabelReactNodedisabledbooleandefaultfalsefocusableWhenDisabledbooleandefaultfalserenderReactElementclassNamestring
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)