Button
A control that commits an action on this screen.
In Multi-brand
See it live
npm run storybookWhen to use it
In Multi-brand: Every action.
- 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 Multi-brand decided and why. Each sample is the live story with that variant set.
primaryvariantUsed in Multi-brandWhat it is for
The one action this view is for.
In Multi-brand
One clear action per view.
secondaryvariantUsed in Multi-brandWhat it is for
A real action, quieter than the commit.
In Multi-brand
The alternative that steps back, and the first step of a delete.
dangervariantUsed in Multi-brandWhat it is for
A destructive commit, after a confirm.
In Multi-brand
The confirm of a delete, the same red in every brand: danger never follows the brand.
ghostvariantUsed in Multi-brandWhat it is for
The lowest emphasis; it never competes with the commit.
In Multi-brand
Row actions, with a word.
defaultsizeUsed in Multi-brandWhat it is for
The normal size, for ordinary actions in forms, dialogs and page chrome.
In Multi-brand
Every button, at touch size: customers are often on phones.
compactsizeNot used in Multi-brandWhat it is for
Denser, for toolbars and table rows that sit beside content.
In Multi-brand
Customers are often on phones: no compact controls.
iconsizeNot used in Multi-brandWhat it is for
Icon only: the name is not on screen.
In Multi-brand
Customers use any device and hover hints never show on touch, so every button has a word.
largesizeNot used in Multi-brandWhat it is for
The largest size: the one action that must stand out most.
In Multi-brand
One clear action per view, at the normal size, in every brand.
hoverstateUsed in Multi-brandWhat it is for
The pointer is over the control: an extra cue, never the only cue.
In Multi-brand
Moderate feedback, shared across brands.
focusstateUsed in Multi-brandWhat it is for
Keyboard or assistive tech is on this control. It must stay visible.
In Multi-brand
Follows the brand, but always shows.
disabledstateUsed in Multi-brandWhat it is for
The action is not available. It must stay perceivable and say why.
In Multi-brand
Only with a visible reason next to it.
How Multi-brand 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
Finished sign-ups and orders.
2So Button
- ✓uses primary: One clear action per view.
- –does not use large: One clear action per view, at the normal size, in every brand.
Alert: default3If it changed
No other answer follows from it.
color.count1The answer
Primary, secondary and ghost in every brand.
2So Button
- ✓uses secondary: The alternative that steps back, and the first step of a delete.
3If it changed
No other answer follows from it.
outcomes.q21The answer
A page that looks like the wrong brand loses trust.
2So Button
- ✓uses secondary: The alternative that steps back, and the first step of a delete.
- ✓uses danger: The confirm of a delete, the same red in every brand: danger never follows the brand.
Badge: danger Alert: destructive3If it changed
and 1 answer that follow from it: Also for emphasis and size.
human.q21The answer
Customers see it fresh each time.
2So Button
- ✓uses ghost: Row actions, with a word.
Toggle: outline3If it changed
No other answer follows from it.
accessibility.values1The answer
Customers sign up on phones.
2So Button
- ✓uses default: Every button, at touch size: customers are often on phones.
- –does not use compact: Customers are often on phones: no compact controls.
- –does not use icon: Customers use any device and hover hints never show on touch, so every button has a word.
Field: horizontal Item: default Item: sm Toggle: default Toggle: sm3If it changed
and 1 answer that follow from it: Those and pressed.
iconography.roles1The answer
Icons for actions, status and navigation.
2So Button
- –does not use icon: Customers use any device and hover hints never show on touch, so every button has a word.
3If it changed
No other answer follows from it.
motion.values1The answer
Moderate speed across brands.
2So Button
- ✓uses hover: Moderate feedback, shared across brands.
3If it changed
No other answer follows from it.
people.q21The answer
AA in every brand.
2So Button
- ✓uses focus: Follows the brand, but always shows.
- ✓uses disabled: Only with a visible reason next to it.
Field: vertical Input group: block-start Input group: block-end3If it changed
and 1 answer that follow from it: AA.
Words
- Shared structure: object, state, next step.
- Brand tone lives in headlines and marketing copy, not in errors.
- Errors and confirmations read the same in every brand.
- Use: Get started, Plan, Order. Avoid: Brand-specific words in errors, Jargon.
The labels components use, from the voice tokens:
content.action.commit: Savecontent.action.cancel: Cancelcontent.action.destroy: Removecontent.action.retry: Try againcontent.action.empty-action: Get started
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.
Multi-brand decides what changes when you switch theme. Here a switch changes colors, type and shape, so the marked rows differ between its brands and 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) | Aster Lightteal.light.600:#0F766EAster Darkteal.dark.600:#7FCFC6Birch Lightpurple.light.600:#6D28D9Birch Darkpurple.dark.600:#B79DF7Coral Lightorange.light.600:#C2410CCoral Darkorange.dark.600:#F7A67F |
| Background hover Changes with theme on hover | component.button.primary.background.hovercolor.action.primary.hovervar(--button-primary-background-hover) | Aster Lightteal.light.700:#015852Aster Darkteal.dark.700:#A1DCD4Birch Lightpurple.light.700:#5212AABirch Darkpurple.dark.700:#C9B7FCCoral Lightorange.light.700:#942D02Coral Darkorange.dark.700:#FCBD9F |
| Text Changes with theme | component.button.primary.labelcolor.text.on-actionvar(--button-primary-label) | Aster Lightslate-deep.light.0:#FFFFFFAster Darkslate-deep.light.900:#0F172ABirch Lightslate-deep.light.0:#FFFFFFBirch Darkslate-deep.light.900:#0F172ACoral Lightslate-deep.light.0:#FFFFFFCoral Darkslate-deep.light.900:#0F172A |
| Secondary | ||
| Fill Changes with theme | component.button.secondary.backgroundcolor.action.secondaryvar(--button-secondary-background) | Aster Lightslate-deep.light.0:#FFFFFFAster Darkslate-deep.dark.50:#0F172ABirch Lightslate-deep.light.0:#FFFFFFBirch Darkslate-deep.dark.50:#0F172ACoral Lightslate-deep.light.0:#FFFFFFCoral Darkslate-deep.dark.50:#0F172A |
| Background hover Changes with theme on hover | component.button.secondary.background.hovercolor.action.secondary.hovervar(--button-secondary-background-hover) | Aster Lightslate-deep.light.100:#F1F5F9Aster Darkslate-deep.dark.100:#1E293BBirch Lightslate-deep.light.100:#F1F5F9Birch Darkslate-deep.dark.100:#1E293BCoral Lightslate-deep.light.100:#F1F5F9Coral Darkslate-deep.dark.100:#1E293B |
| Text Changes with theme | component.button.secondary.labelcolor.text.primaryvar(--button-secondary-label) | Aster Lightslate-deep.light.900:#0F172AAster Darkslate-deep.dark.900:#F8FAFCBirch Lightslate-deep.light.900:#0F172ABirch Darkslate-deep.dark.900:#F8FAFCCoral Lightslate-deep.light.900:#0F172ACoral Darkslate-deep.dark.900:#F8FAFC |
| Border Changes with theme | component.button.secondary.bordercolor.border.strongvar(--button-secondary-border) | Aster Lightslate-deep.light.500:#64748BAster Darkslate-deep.dark.400:#96A3B5Birch Lightslate-deep.light.500:#64748BBirch Darkslate-deep.dark.400:#96A3B5Coral Lightslate-deep.light.500:#64748BCoral Darkslate-deep.dark.400:#96A3B5 |
| Border width | component.button.secondary.border-widthborder-width.defaultvar(--button-secondary-border-width) | border-width.1:1px |
| Danger | ||
| Fill Changes with theme | component.button.danger.backgroundcolor.action.dangervar(--button-danger-background) | Aster Lightred.light.600:#C42B2BAster Darkred.dark.600:#F28B8BBirch Lightred.light.600:#C42B2BBirch Darkred.dark.600:#F28B8BCoral Lightred.light.600:#C42B2BCoral Darkred.dark.600:#F28B8B |
| Background hover Changes with theme on hover | component.button.danger.background.hovercolor.action.danger.hovervar(--button-danger-background-hover) | Aster Lightred.light.700:#981519Aster Darkred.dark.700:#FAA9A8Birch Lightred.light.700:#981519Birch Darkred.dark.700:#FAA9A8Coral Lightred.light.700:#981519Coral Darkred.dark.700:#FAA9A8 |
| Text Changes with theme | component.button.danger.labelcolor.text.on-dangervar(--button-danger-label) | Aster Lightslate-deep.light.0:#FFFFFFAster Darkslate-deep.light.900:#0F172ABirch Lightslate-deep.light.0:#FFFFFFBirch Darkslate-deep.light.900:#0F172ACoral Lightslate-deep.light.0:#FFFFFFCoral Darkslate-deep.light.900:#0F172A |
| Ghost | ||
| Background hover Changes with theme on hover | component.button.ghost.background-hovercolor.surface.sunkenvar(--button-ghost-background-hover) | Aster Lightslate-deep.light.100:#F1F5F9Aster Darkslate-deep.dark.100:#1E293BBirch Lightslate-deep.light.100:#F1F5F9Birch Darkslate-deep.dark.100:#1E293BCoral Lightslate-deep.light.100:#F1F5F9Coral Darkslate-deep.dark.100:#1E293B |
| Text Changes with theme | component.button.ghost.labelcolor.text.linkvar(--button-ghost-label) | Aster Lightteal.light.700:#015852Aster Darkteal.dark.700:#A1DCD4Birch Lightpurple.light.700:#5212AABirch Darkpurple.dark.700:#C9B7FCCoral Lightorange.light.700:#942D02Coral Darkorange.dark.700:#FCBD9F |
| Button | ||
| Height | component.button.heightsize.control.defaultvar(--button-height) | size.44:2.75rem |
| Padding, left and right | component.button.padding-xspace.inset.control-xvar(--button-padding-x) | space.16:1rem |
| Corner Changes with theme | component.button.radiusradius.controlvar(--button-radius) | Aster Lightradius.4:0.25remAster Darkradius.4:0.25remBirch Lightradius.12:0.75remBirch Darkradius.12:0.75remCoral Lightradius.999:9999pxCoral Darkradius.999:9999px |
| Text size | component.button.font-sizetype.label.font-sizevar(--button-font-size) | font.size.14:0.875rem |
| Weight | component.button.font-weighttype.label.font-weightvar(--button-font-weight) | font.weight.600:600 |
| Focus ring Changes with theme on focus | component.button.focus-ringcolor.border.focusvar(--button-focus-ring) | Aster Lightteal.light.600:#0F766EAster Darkteal.dark.600:#7FCFC6Birch Lightpurple.light.600:#6D28D9Birch Darkpurple.dark.600:#B79DF7Coral Lightorange.light.600:#C2410CCoral Darkorange.dark.600:#F7A67F |
| Icon size | component.button.icon-sizesize.iconvar(--button-icon-size) | size.16:1rem |
| Gap | component.button.gapspace.gap.defaultvar(--button-gap) | space.12:0.75rem |
| Font family Changes with theme | component.button.font-familytype.label.font-familyvar(--button-font-family) | Aster Lightfont.family.inter:Inter, system-ui, sans-serifAster Darkfont.family.inter:Inter, system-ui, sans-serifBirch Lightfont.family.plus-jakarta-sans:"Plus Jakarta Sans", system-ui, sans-serifBirch Darkfont.family.plus-jakarta-sans:"Plus Jakarta Sans", system-ui, sans-serifCoral Lightfont.family.poppins:Poppins, system-ui, sans-serifCoral Darkfont.family.poppins:Poppins, system-ui, sans-serif |
| Focus on focus | component.button.focusborder-width.focusvar(--button-focus) | border-width.2:2px |
| Canvas Changes with theme on focus | component.button.canvascolor.surface.canvasvar(--button-canvas) | Aster Lightslate-deep.light.50:#F8FAFCAster Darkslate-deep.dark.0:#020617Birch Lightslate-deep.light.50:#F8FAFCBirch Darkslate-deep.dark.0:#020617Coral Lightslate-deep.light.50:#F8FAFCCoral Darkslate-deep.dark.0:#020617 |
| Disabled | ||
| Fill Changes with theme when disabled | component.button.disabled.backgroundcolor.disabled.surfacevar(--button-disabled-background) | Aster Lightslate-deep.light.100:#F1F5F9Aster Darkslate-deep.dark.100:#1E293BBirch Lightslate-deep.light.100:#F1F5F9Birch Darkslate-deep.dark.100:#1E293BCoral Lightslate-deep.light.100:#F1F5F9Coral Darkslate-deep.dark.100:#1E293B |
| Text Changes with theme when disabled | component.button.disabled.labelcolor.disabled.textvar(--button-disabled-label) | Aster Lightslate-deep.light.500:#64748BAster Darkslate-deep.dark.500:#CBD5E1Birch Lightslate-deep.light.500:#64748BBirch Darkslate-deep.dark.500:#CBD5E1Coral Lightslate-deep.light.500:#64748BCoral Darkslate-deep.dark.500:#CBD5E1 |
| Border Changes with theme when disabled | component.button.disabled.bordercolor.disabled.bordervar(--button-disabled-border) | Aster Lightslate-deep.light.200:#E2E8F0Aster Darkslate-deep.dark.200:#334155Birch Lightslate-deep.light.200:#E2E8F0Birch Darkslate-deep.dark.200:#334155Coral Lightslate-deep.light.200:#E2E8F0Coral Darkslate-deep.dark.200:#334155 |
| Compact | ||
| Height | component.button.compact.heightsize.control.compactvar(--button-compact-height) | size.36:2.25rem |
| 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.44:2.75rem |
| Shared | ||
| Transition duration | component.transition.durationmotion.duration.feedbackvar(--transition-duration) | duration.moderate:200ms |
| Transition easing | component.transition.easingmotion.easing.standardvar(--transition-easing) | easing.standard:cubic-bezier(0.2,0,0,1) |