One button component, named by role (D4). Primary is Bolster's black button with highlighter text; brand is the yellow call to action for the one step that moves a job forward ("Preview and send"). Blue is not a button colour: it's for links, focus and selection. Every state has a visible focus ring (D8). Primary, brand, danger and secondary have a body: an edge, a light line along the top, a shadow; hover lifts 1px and a press sinks at once, then eases back. Ghost is for places that already say "button" (toolbars, rows, bars, Close). Every button has 8px corners (D33).
Roles and states
Hover, active and focus are pinned with data-state for this page only. Use one primary per view; secondary for everything else, ghost inside toolbars and rows. Tertiary is gone: secondary covers it.
| Role | Default | Hover | Active | Keyboard focus | Disabled | Loading |
|---|---|---|---|---|---|---|
| primary | ||||||
| brand | ||||||
| secondary | ||||||
| ghost | ||||||
| ghost · danger | ||||||
| danger |
Sizes
Heights follow the control scale: 32, 40 (default), 48, 56. Small buttons get a 44px touch area on coarse pointers without drawing bigger.
| Size | Primary | Secondary | Icon only | Use |
|---|---|---|---|---|
| sm · 32 | tables, dense toolbars | |||
| md · 40 | default: toolbars, forms | |||
| lg · 48 | touch, mobile primary | |||
| xl · 56 | field app hero actions |
On the dark top bar
Icons side by side (D41)
A button's size sets its icon's box, but glyphs fill their boxes differently: × draws 62 % of it, the expand arrows all of it. Icons that sit together match their ink and line instead. One drawn smaller goes a weight up so its line stays as thick, and keeps its own size through --icon-size (ic(name, size, { optical: true }), or data-optical in a specimen). scripts/check-icons.js measures a page.
| same box | both 16 Light: ink 16 and 10, so expand reads half as big again | |
| same ink | expand 12 Regular, × 18 Light: ink 12 and 11, both lines 1.1 px |
Rules
- Labels start with a verb, sentence case, no trailing punctuation: "Create job", not "New" or "CREATE".
- Icon-only buttons need an
aria-label, and a tooltip if the icon isn't universal. - Icons side by side match in ink and line, not in box size (D41);
scripts/check-icons.jslists the ones that don't. - Don't override a button's height, radius or colours locally. If a variant is missing, add it here (the audit found 257 of 364 buttons with local overrides).
- Show an action only when it applies. A disabled button needs a reason nearby.
- A row or card that opens something is a link, not a button: see Data table and Card.
- Red is for what can't be undone. Where Delete is one action of several it's a quiet danger (red text, fills red when pointed at); the filled red danger is only the confirmation's final button.
Markup
<button class="button">Create job</button>
<button class="button" data-variant="secondary" data-size="sm">Go to pipeline</button>
<button class="button" data-variant="ghost" data-icon-only aria-label="Refresh"><svg …/></button>
<a class="button" data-variant="secondary" href="/jobs">View jobs</a>
<button class="button" data-variant="ghost" data-tone="danger">Delete</button>
<button class="button" aria-busy="true">Saving…</button>