Button
A control that commits an action on this screen.
In Content first
See it live
npm run storybookWhen to use it
In Content first: Subscribe, share and save.
- 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 Content first decided and why. Each sample is the live story with that variant set.
primaryvariantUsed in Content firstWhat it is for
The one action this view is for.
In Content first
One clear action per section.
secondaryvariantUsed in Content firstWhat it is for
A real action, quieter than the commit.
In Content first
The first step of a delete, with the full verb, before the confirm.
dangervariantUsed in Content firstWhat it is for
A destructive commit, after a confirm.
In Content first
The confirm of a delete, with the full verb. Deleting a draft is rare and final.
ghostvariantUsed in Content firstWhat it is for
The lowest emphasis; it never competes with the commit.
In Content first
Alternatives as a text link, and row actions with a word.
defaultsizeUsed in Content firstWhat it is for
The normal size, for ordinary actions in forms, dialogs and page chrome.
In Content first
Every button, at touch size: many readers are on phones.
compactsizeNot used in Content firstWhat it is for
Denser, for toolbars and table rows that sit beside content.
In Content first
Many readers are on phones: no compact controls.
iconsizeNot used in Content firstWhat it is for
Icon only: the name is not on screen.
In Content first
Occasional visitors do not know icons: always add a word.
largesizeNot used in Content firstWhat it is for
The largest size: the one action that must stand out most.
In Content first
Attention belongs to the text: one clear action per section, at the normal size.
hoverstateUsed in Content firstWhat it is for
The pointer is over the control: an extra cue, never the only cue.
In Content first
Moderate feedback that feels calm.
focusstateUsed in Content firstWhat it is for
Keyboard or assistive tech is on this control. It must stay visible.
In Content first
Always visible.
disabledstateNot used in Content firstWhat it is for
The action is not available. It must stay perceivable and say why.
In Content first
Keep it enabled and explain on press: visitors see the interface fresh each time.
How Content first uses it
Why it works this way
Button looks the way it does because of 9 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.
tasks.q21The answer
Actions are rare: subscribe, share, save.
2So Button
- ✓uses primary: One clear action per section.
- –does not use large: Attention belongs to the text: one clear action per section, at the normal size.
Field: horizontal Input group: block-start Input group: block-end3If it changed
and 2 answers that follow from it: Three, Only what today's screens use.
outcomes.q21The answer
Losing your place in an article breaks the read.
2So Button
- ✓uses secondary: The first step of a delete, with the full verb, before the confirm.
- ✓uses danger: The confirm of a delete, with the full verb. Deleting a draft is rare and final.
3If it changed
No other answer follows from it.
brand.q11The answer
Warm and considered, like a good magazine.
2So Button
- ✓uses ghost: Alternatives as a text link, and row actions with a word.
3If it changed
and 11 answers that follow from it: Present, Neutrals and one brand ramp, Balanced, and 8 more.
human.q21The answer
Visitors see the interface fresh each time.
2So Button
- ✓uses ghost: Alternatives as a text link, and row actions with a word.
- –does not use icon: Occasional visitors do not know icons: always add a word.
- –does not use disabled: Keep it enabled and explain on press: visitors see the interface fresh each time.
Input group: inline-start Input group: inline-end3If it changed
No other answer follows from it.
accessibility.values1The answer
Readers are often on phones.
2So Button
- ✓uses default: Every button, at touch size: many readers are on phones.
- –does not use compact: Many readers are on phones: no compact controls.
Item: default Item: sm3If it changed
and 1 answer that follow from it: Those and pressed.
iconography.roles1The answer
Occasional readers need icons for save, share and navigation.
2So Button
- –does not use icon: Occasional visitors do not know icons: always add a word.
Input group: inline-start3If it changed
No other answer follows from it.
human.q11The answer
Attention belongs to the text.
2So Button
- –does not use large: Attention belongs to the text: one clear action per section, at the normal size.
Field: horizontal Empty: default Item: muted3If it changed
and 8 answers that follow from it: Surface, text, action and status, Only what floats, One, and 5 more.
motion.values1The answer
Moderate speed feels calm.
2So Button
- ✓uses hover: Moderate feedback that feels calm.
3If it changed
No other answer follows from it.
people.q21The answer
AA is the commitment for a public site.
2So Button
- ✓uses focus: Always visible.
- –does not use disabled: Keep it enabled and explain on press: visitors see the interface fresh each time.
Field: vertical3If it changed
and 3 answers that follow from it: A ratio, line heights in proportion, Two: color and word, AA.
Words
- Write to one person: "you", not "users".
- Short sentences with room to breathe.
- Explain why before asking for anything.
- Use: Read, Save for later, You, Next issue. Avoid: Click here, Submit, Users, Content.
The labels components use, from the voice tokens:
content.action.commit: Savecontent.action.cancel: Not nowcontent.action.destroy: Removecontent.action.retry: Try againcontent.action.empty-action: Browse guides
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.
Content first 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) | Lightnavy.light.600:#1E3A5FDarknavy.dark.600:#8FA6C4 |
| Background hover Changes with theme on hover | component.button.primary.background.hovercolor.action.primary.hovervar(--button-primary-background-hover) | Lightnavy.light.700:#122948Darknavy.dark.700:#ACBDD5 |
| Text Changes with theme | component.button.primary.labelcolor.text.on-actionvar(--button-primary-label) | Lightstone.light.0:#FFFFFFDarkstone.light.900:#1F1B16 |
| Secondary | ||
| Fill Changes with theme | component.button.secondary.backgroundcolor.action.secondaryvar(--button-secondary-background) | Lightstone.light.0:#FFFFFFDarkstone.dark.50:#1F1B16 |
| Background hover Changes with theme on hover | component.button.secondary.background.hovercolor.action.secondary.hovervar(--button-secondary-background-hover) | Lightstone.light.100:#F6F2EADarkstone.dark.100:#2E2A24 |
| Text Changes with theme | component.button.secondary.labelcolor.text.primaryvar(--button-secondary-label) | Lightstone.light.900:#1F1B16Darkstone.dark.900:#FFFDF8 |
| Border Changes with theme | component.button.secondary.bordercolor.border.strongvar(--button-secondary-border) | Lightstone.light.500:#7A7062Darkstone.dark.400:#A39A8B |
| 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) | 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) | Lightstone.light.0:#FFFFFFDarkstone.light.900:#1F1B16 |
| Ghost | ||
| Background hover Changes with theme on hover | component.button.ghost.background-hovercolor.surface.sunkenvar(--button-ghost-background-hover) | Lightstone.light.100:#F6F2EADarkstone.dark.100:#2E2A24 |
| Text Changes with theme | component.button.ghost.labelcolor.text.linkvar(--button-ghost-label) | Lightnavy.light.700:#122948Darknavy.dark.700:#ACBDD5 |
| Button | ||
| Height | component.button.heightsize.control.defaultvar(--button-height) | size.48:3rem |
| Padding, left and right | component.button.padding-xspace.inset.control-xvar(--button-padding-x) | space.20:1.25rem |
| Corner | component.button.radiusradius.controlvar(--button-radius) | radius.8:0.5rem |
| Text size | component.button.font-sizetype.label.font-sizevar(--button-font-size) | font.size.16:1rem |
| 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) | Lightnavy.light.600:#1E3A5FDarknavy.dark.600:#8FA6C4 |
| Icon size | component.button.icon-sizesize.iconvar(--button-icon-size) | size.16:1rem |
| 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.inter:Inter, 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) | Lightstone.light.50:#FFFDF8Darkstone.dark.0:#14110D |
| Disabled | ||
| Fill Changes with theme when disabled | component.button.disabled.backgroundcolor.disabled.surfacevar(--button-disabled-background) | Lightstone.light.100:#F6F2EADarkstone.dark.100:#2E2A24 |
| Text Changes with theme when disabled | component.button.disabled.labelcolor.disabled.textvar(--button-disabled-label) | Lightstone.light.500:#7A7062Darkstone.dark.500:#CFC6B6 |
| Border Changes with theme when disabled | component.button.disabled.bordercolor.disabled.bordervar(--button-disabled-border) | Lightstone.light.200:#E4DDD0Darkstone.dark.200:#4A4339 |
| Compact | ||
| Height | component.button.compact.heightsize.control.compactvar(--button-compact-height) | size.40:2.5rem |
| 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.48:3rem |
| 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) |