Button
A control that commits an action on this screen.
In Brand expressive
See it live
npm run storybookWhen to use it
In Brand expressive: The one action: sign up.
- Use it when: The press does something here: save, submit, delete, open a dialog, go to the next step.
- Not when: The press only takes people to another page, or flips a setting that should persist.
Instead:
- Toggle: An on or off setting that should persist as state.
- Link: Navigation to another page or section.
Variants
What each variant is for in any design system, then what Brand expressive decided and why. Each sample is the live story with that variant set.
primaryvariantUsed in Brand expressiveWhat it is for
The one action this view is for.
In Brand expressive
One loud call to action per section.
secondaryvariantUsed in Brand expressiveWhat it is for
A real action, quieter than the commit.
In Brand expressive
The first step of a delete, before the confirm.
dangervariantUsed in Brand expressiveWhat it is for
A destructive commit, after a confirm.
In Brand expressive
The confirm of a delete. It stays red, apart from the brand's pink.
ghostvariantUsed in Brand expressiveWhat it is for
The lowest emphasis; it never competes with the commit.
In Brand expressive
The alternative next to the primary, and row actions with a word.
defaultsizeUsed in Brand expressiveWhat it is for
The normal size, for ordinary actions in forms, dialogs and page chrome.
In Brand expressive
Every button, at touch size: phones first.
compactsizeNot used in Brand expressiveWhat it is for
Denser, for toolbars and table rows that sit beside content.
In Brand expressive
Phones first: no compact controls.
iconsizeNot used in Brand expressiveWhat it is for
Icon only: the name is not on screen.
In Brand expressive
Phones first: hover hints never show, so every button has a word.
largesizeUsed in Brand expressiveWhat it is for
The largest size: the one action that must stand out most.
In Brand expressive
The one loud call to action per section.
hoverstateUsed in Brand expressiveWhat it is for
The pointer is over the control: an extra cue, never the only cue.
In Brand expressive
Relaxed, springy feedback: motion is part of the brand.
focusstateUsed in Brand expressiveWhat it is for
Keyboard or assistive tech is on this control. It must stay visible.
In Brand expressive
Always shows, even on vivid color.
disabledstateUsed in Brand expressiveWhat it is for
The action is not available. It must stay perceivable and say why.
In Brand expressive
Only with a visible reason next to it.
How Brand expressive uses it
Why it works this way
Button looks the way it does because of 8 answers about this product. Each card is one answer, in three steps: the answer, what it decided for Button, and what would move if it changed.
outcomes.q11The answer
Engagement and sign-ups.
2So Button
- ✓uses primary: One loud call to action per section.
- ✓uses large: The one loud call to action per section.
Badge: warning Toast: success Toggle: lg3If it changed
and 6 answers that follow from it: A few minutes, Brand moments, Hero and launch moments, and 3 more.
outcomes.q21The answer
A bland page is the costly mistake.
2So Button
- ✓uses secondary: The first step of a delete, before the confirm.
- ✓uses danger: The confirm of a delete. It stays red, apart from the brand's pink.
Alert: destructive Item: muted3If it changed
and 1 answer that follow from it: One per moment.
color.count1The answer
Primary, secondary, ghost.
2So Button
- ✓uses ghost: The alternative next to the primary, and row actions with a word.
3If it changed
No other answer follows from it.
iconography.roles1The answer
Icons also decorate campaign pages.
2So Button
- ✓uses ghost: The alternative next to the primary, and row actions with a word.
- –does not use icon: Phones first: hover hints never show, so every button has a word.
Empty: icon Input group: inline-start3If it changed
and 1 answer that follow from it: Two.
accessibility.values1The answer
Visitors come from social posts on phones.
2So Button
- ✓uses default: Every button, at touch size: phones first.
- –does not use compact: Phones first: no compact controls.
- –does not use icon: Phones first: hover hints never show, so every button has a word.
Field: horizontal Input group: inline-end Item: default Item: sm Toggle: default Toggle: sm3If it changed
and 1 answer that follow from it: Those and pressed.
motion.roles1The answer
Motion carries personality.
2So Button
- ✓uses hover: Relaxed, springy feedback: motion is part of the brand.
3If it changed
and 1 answer that follow from it: Three.
motion.values1The answer
Feedback stays fast; the personality shows in panels and hero moments, which move slower on a spring.
2So Button
- ✓uses hover: Relaxed, springy feedback: motion is part of the brand.
3If it changed
No other answer follows from it.
people.q21The answer
AA, even when loud.
2So Button
- ✓uses focus: Always shows, even on vivid color.
- ✓uses disabled: Only with a visible reason next to it.
3If it changed
and 2 answers that follow from it: Two: color and word, AA.
Words
- Headlines are short and bold. Three to six words.
- Verbs that move: Join, Try, Go.
- Playful in headlines, plain in errors.
- Use: Join, Drop, Go, Love. Avoid: Synergy, Leverage, Kindly.
The labels components use, from the voice tokens:
content.action.commit: Gocontent.action.cancel: Not nowcontent.action.destroy: Removecontent.action.retry: Try againcontent.action.empty-action: Explore the drop
See Voice and tone for the whole voice.
Accessibility and keyboard
| Keyboard | What happens | Not |
|---|---|---|
| Moving through the page or a dialog. | Tab lands on the button in a logical order. | The control is skipped because it used HTML disabled. |
| The button is focused and the person confirms. | Enter runs the same action as a click. | Enter only works on submit buttons. |
| The button is focused. | Space runs the same action as a click. | Space scrolls the page instead. |
| The button opened a dialog, menu or popover that just closed. | After the action closes a surface, focus returns to the button. | Focus dropped on the page body. |
Test before you ship:
- The control has a name assistive tech can speak. Any button, especially an icon-only one.
- The focused control stays visible; color alone is not the cue. Keyboard or assistive-tech focus is on the button.
- Unavailable stays in the tab order and explains why. The action must not run, but focus must not jump away.
- A button that stays down exposes its pressed state. Pin, follow, or another sticky on or off on a button.
- A button that opens something exposes open or closed. The press reveals a menu, panel or more content.
- The hit area stays usable, including at a small size. Touch, pointer, or a compact toolbar button.
Use it in code
npx open-ds add button
The component reads only component keys and semantic tokens, so it follows the brand and mode on <html>.
Tokens it reads
Button reads 32 tokens of its own, each an alias of a semantic token. Each row is one part: its own token, the semantic it aliases, and the value it ends at. Change a semantic and every component that reads it follows; override one of Button's tokens to change Button alone.
Brand expressive decides what changes when you switch theme. Here a switch changes colors only, so the marked rows differ between light and dark; everything else holds steady.
| Part | Its token and the semantic it aliases | Value |
|---|---|---|
| Primary | ||
| Fill Changes with theme | component.button.primary.backgroundcolor.action.primaryvar(--button-primary-background) | Lightpink.light.600:#D61F7ADarkpink.dark.600:#F59ACA |
| Background hover Changes with theme on hover | component.button.primary.background.hovercolor.action.primary.hovervar(--button-primary-background-hover) | Lightpink.light.700:#A6025BDarkpink.dark.700:#FBB4D8 |
| Text Changes with theme | component.button.primary.labelcolor.text.on-actionvar(--button-primary-label) | Lighttaupe.light.0:#FFFFFFDarktaupe.light.900:#1F1411 |
| Secondary | ||
| Fill Changes with theme | component.button.secondary.backgroundcolor.action.secondaryvar(--button-secondary-background) | Lighttaupe.light.0:#FFFFFFDarktaupe.dark.50:#1F1411 |
| Background hover Changes with theme on hover | component.button.secondary.background.hovercolor.action.secondary.hovervar(--button-secondary-background-hover) | Lighttaupe.light.100:#FCEFE6Darktaupe.dark.100:#33241F |
| Text Changes with theme | component.button.secondary.labelcolor.text.primaryvar(--button-secondary-label) | Lighttaupe.light.900:#1F1411Darktaupe.dark.900:#FFF8F3 |
| Border Changes with theme | component.button.secondary.bordercolor.border.strongvar(--button-secondary-border) | Lighttaupe.light.500:#8A6F63Darktaupe.dark.400:#B5988A |
| Border width | component.button.secondary.border-widthborder-width.defaultvar(--button-secondary-border-width) | border-width.2:2px |
| Danger | ||
| Fill Changes with theme | component.button.danger.backgroundcolor.action.dangervar(--button-danger-background) | Lightred.light.600:#C42B2BDarkred.dark.600:#F28B8B |
| Background hover Changes with theme on hover | component.button.danger.background.hovercolor.action.danger.hovervar(--button-danger-background-hover) | Lightred.light.700:#981519Darkred.dark.700:#FAA9A8 |
| Text Changes with theme | component.button.danger.labelcolor.text.on-dangervar(--button-danger-label) | Lighttaupe.light.0:#FFFFFFDarktaupe.light.900:#1F1411 |
| Ghost | ||
| Background hover Changes with theme on hover | component.button.ghost.background-hovercolor.surface.sunkenvar(--button-ghost-background-hover) | Lighttaupe.light.100:#FCEFE6Darktaupe.dark.100:#33241F |
| Text Changes with theme | component.button.ghost.labelcolor.text.linkvar(--button-ghost-label) | Lightpink.light.700:#A6025BDarkpink.dark.700:#FBB4D8 |
| Button | ||
| Height | component.button.heightsize.control.defaultvar(--button-height) | size.52:3.25rem |
| Padding, left and right | component.button.padding-xspace.inset.control-xvar(--button-padding-x) | space.28:1.75rem |
| Corner | component.button.radiusradius.controlvar(--button-radius) | radius.999:9999px |
| Text size | component.button.font-sizetype.label.font-sizevar(--button-font-size) | font.size.15:0.9375rem |
| Weight | component.button.font-weighttype.label.font-weightvar(--button-font-weight) | font.weight.700:700 |
| Focus ring Changes with theme on focus | component.button.focus-ringcolor.border.focusvar(--button-focus-ring) | Lightpink.light.600:#D61F7ADarkpink.dark.600:#F59ACA |
| Icon size | component.button.icon-sizesize.iconvar(--button-icon-size) | size.20:1.25rem |
| Gap | component.button.gapspace.gap.defaultvar(--button-gap) | space.16:1rem |
| Font family | component.button.font-familytype.label.font-familyvar(--button-font-family) | font.family.dm-sans:"DM Sans", system-ui, sans-serif |
| Focus on focus | component.button.focusborder-width.focusvar(--button-focus) | border-width.3:3px |
| Canvas Changes with theme on focus | component.button.canvascolor.surface.canvasvar(--button-canvas) | Lighttaupe.light.50:#FFF8F3Darktaupe.dark.0:#120B09 |
| Disabled | ||
| Fill Changes with theme when disabled | component.button.disabled.backgroundcolor.disabled.surfacevar(--button-disabled-background) | Lighttaupe.light.100:#FCEFE6Darktaupe.dark.100:#33241F |
| Text Changes with theme when disabled | component.button.disabled.labelcolor.disabled.textvar(--button-disabled-label) | Lighttaupe.light.500:#8A6F63Darktaupe.dark.500:#E2C4B3 |
| Border Changes with theme when disabled | component.button.disabled.bordercolor.disabled.bordervar(--button-disabled-border) | Lighttaupe.light.200:#F2DCCFDarktaupe.dark.200:#4D3A33 |
| Compact | ||
| Height | component.button.compact.heightsize.control.compactvar(--button-compact-height) | size.44:2.75rem |
| Padding, left and right | component.button.compact.padding-xspace.inset.cell-xvar(--button-compact-padding-x) | space.16:1rem |
| Large | ||
| Height | component.button.large.heightsize.control.largevar(--button-large-height) | size.60:3.75rem |
| Shared | ||
| Transition duration | component.transition.durationmotion.duration.feedbackvar(--transition-duration) | duration.fast:120ms |
| Transition easing | component.transition.easingmotion.easing.standardvar(--transition-easing) | easing.spring:cubic-bezier(0.34,1.56,0.64,1) |