Button
A control that commits an action on this screen.
In Dense data
See it live
npm run storybookWhen to use it
In Dense data: 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 Dense data decided and why. Each sample is the live story with that variant set.
primaryvariantUsed in Dense dataWhat it is for
The one action this view is for.
In Dense data
The one commit in a view.
secondaryvariantUsed in Dense dataWhat it is for
A real action, quieter than the commit.
In Dense data
Alternatives and cancel next to the primary, and the first step of a delete.
dangervariantUsed in Dense dataWhat it is for
A destructive commit, after a confirm.
In Dense data
The confirm of a bulk delete. Many records go at once, so the last press must not look like the everyday primary.
ghostvariantUsed in Dense dataWhat it is for
The lowest emphasis; it never competes with the commit.
In Dense data
Row actions, as compact icon buttons. Never for cancel.
defaultsizeUsed in Dense dataWhat it is for
The normal size, for ordinary actions in forms, dialogs and page chrome.
In Dense data
Actions outside tables: forms, dialogs, page actions.
compactsizeUsed in Dense dataWhat it is for
Denser, for toolbars and table rows that sit beside content.
In Dense data
Table rows and toolbars. Compact still fits a mouse.
iconsizeUsed in Dense dataWhat it is for
Icon only: the name is not on screen.
In Dense data
Known row and toolbar actions, always with a tooltip.
largesizeNot used in Dense dataWhat it is for
The largest size: the one action that must stand out most.
In Dense data
Controls stay small so more records fit on one screen.
hoverstateUsed in Dense dataWhat it is for
The pointer is over the control: an extra cue, never the only cue.
In Dense data
Fast feedback that confirms the pointer.
focusstateUsed in Dense dataWhat it is for
Keyboard or assistive tech is on this control. It must stay visible.
In Dense data
Always visible: the focus ring at border-width.focus.
disabledstateUsed in Dense dataWhat it is for
The action is not available. It must stay perceivable and say why.
In Dense data
Only with a visible reason next to it.
How Dense data 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.
color.count1The answer
One primary per view; the rest step back.
2So Button
- ✓uses primary: The one commit in a view.
3If it changed
No other answer follows from it.
outcomes.q21The answer
Acting on the wrong row is the costly error.
2So Button
- ✓uses primary: The one commit in a view.
- ✓uses secondary: Alternatives and cancel next to the primary, and the first step of a delete.
- ✓uses danger: The confirm of a bulk delete. Many records go at once, so the last press must not look like the everyday primary.
Alert: default Alert: destructive Toast: danger3If it changed
and 2 answers that follow from it: Status in tables, Errors and empty states.
human.q21The answer
Daily users learn the interface and recognize it.
2So Button
- ✓uses secondary: Alternatives and cancel next to the primary, and the first step of a delete.
- ✓uses ghost: Row actions, as compact icon buttons. Never for cancel.
3If it changed
No other answer follows from it.
tasks.q21The answer
The same actions repeat many times an hour.
2So Button
- ✓uses ghost: Row actions, as compact icon buttons. Never for cancel.
Input group: inline-end Sidebar: default Alert: default Toast: success Toggle: default3If it changed
and 11 answers that follow from it: Time on task, All day, Three, and 8 more.
content.q21The answer
Experts want many records on one screen.
2So Button
- ✓uses default: Actions outside tables: forms, dialogs, page actions.
- ✓uses compact: Table rows and toolbars. Compact still fits a mouse.
- –does not use large: Controls stay small so more records fit on one screen.
Input group: block-start Input group: block-end Field: horizontal Sidebar: lg Toggle: default Toggle: sm Toggle: lg3If it changed
and 14 answers that follow from it: Quiet, Only the steps the screens need, Small, and 11 more.
accessibility.values1The answer
Mouse and keyboard; compact targets are enough.
2So Button
- ✓uses default: Actions outside tables: forms, dialogs, page actions.
- ✓uses compact: Table rows and toolbars. Compact still fits a mouse.
Item: sm Toggle: sm3If it changed
No other answer follows from it.
iconography.roles1The answer
Icons mark row actions and status.
2So Button
- ✓uses icon: Known row and toolbar actions, always with a tooltip.
Input group: inline-start Empty: icon3If it changed
No other answer follows from it.
motion.values1The answer
Fast feedback keeps up with experts.
2So Button
- ✓uses hover: Fast feedback that confirms the pointer.
3If it changed
No other answer follows from it.
people.q21The answer
AA is the floor for an internal tool used by trained staff.
2So Button
- ✓uses focus: Always visible: the focus ring at border-width.focus.
- ✓uses disabled: Only with a visible reason next to it.
3If it changed
and 3 answers that follow from it: A ratio, line heights in proportion, Three: color, icon and word, AA.
Words
- Lead with the object, then the state: "Alert 2041 contained."
- Use numbers, not words for numbers.
- One verb per button. No "please", no exclamation marks.
- Use: Investigate, Filter, Assign, Escalated, Resolved. Avoid: Oops, Awesome, Please, Simply.
The labels components use, from the voice tokens:
content.action.commit: Savecontent.action.cancel: Cancelcontent.action.destroy: Deletecontent.action.retry: Retrycontent.action.empty-action: Change filters
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.
Dense data 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) | Lightviolet.light.600:#4B3FD9Darkviolet.dark.600:#A9A3F6 |
| Background hover Changes with theme on hover | component.button.primary.background.hovercolor.action.primary.hovervar(--button-primary-background-hover) | Lightviolet.light.700:#3729AADarkviolet.dark.700:#BEBBFB |
| Text Changes with theme | component.button.primary.labelcolor.text.on-actionvar(--button-primary-label) | Lightslate.light.0:#FFFFFFDarkslate.light.900:#161B23 |
| Secondary | ||
| Fill Changes with theme | component.button.secondary.backgroundcolor.action.secondaryvar(--button-secondary-background) | Lightslate.light.0:#FFFFFFDarkslate.dark.50:#161B23 |
| Background hover Changes with theme on hover | component.button.secondary.background.hovercolor.action.secondary.hovervar(--button-secondary-background-hover) | Lightslate.light.100:#EEF1F5Darkslate.dark.100:#262D38 |
| Text Changes with theme | component.button.secondary.labelcolor.text.primaryvar(--button-secondary-label) | Lightslate.light.900:#161B23Darkslate.dark.900:#F7F8FA |
| Border Changes with theme | component.button.secondary.bordercolor.border.strongvar(--button-secondary-border) | Lightslate.light.500:#6B7585Darkslate.dark.400:#969FAD |
| 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) | Lightslate.light.0:#FFFFFFDarkslate.light.900:#161B23 |
| Ghost | ||
| Background hover Changes with theme on hover | component.button.ghost.background-hovercolor.surface.sunkenvar(--button-ghost-background-hover) | Lightslate.light.100:#EEF1F5Darkslate.dark.100:#262D38 |
| Text Changes with theme | component.button.ghost.labelcolor.text.linkvar(--button-ghost-label) | Lightviolet.light.700:#3729AADarkviolet.dark.700:#BEBBFB |
| Button | ||
| Height | component.button.heightsize.control.defaultvar(--button-height) | size.32:2rem |
| Padding, left and right | component.button.padding-xspace.inset.control-xvar(--button-padding-x) | space.12:0.75rem |
| Corner | component.button.radiusradius.controlvar(--button-radius) | radius.2:0.125rem |
| Text size | component.button.font-sizetype.label.font-sizevar(--button-font-size) | font.size.12:0.75rem |
| Weight | component.button.font-weighttype.label.font-weightvar(--button-font-weight) | font.weight.500:500 |
| Focus ring Changes with theme on focus | component.button.focus-ringcolor.border.focusvar(--button-focus-ring) | Lightviolet.light.600:#4B3FD9Darkviolet.dark.600:#A9A3F6 |
| Icon size | component.button.icon-sizesize.iconvar(--button-icon-size) | size.16:1rem |
| Gap | component.button.gapspace.gap.defaultvar(--button-gap) | space.8:0.5rem |
| Font family | component.button.font-familytype.label.font-familyvar(--button-font-family) | font.family.ibm-plex-sans:"IBM Plex Sans", 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) | Lightslate.light.50:#F7F8FADarkslate.dark.0:#0D1117 |
| Disabled | ||
| Fill Changes with theme when disabled | component.button.disabled.backgroundcolor.disabled.surfacevar(--button-disabled-background) | Lightslate.light.100:#EEF1F5Darkslate.dark.100:#262D38 |
| Text Changes with theme when disabled | component.button.disabled.labelcolor.disabled.textvar(--button-disabled-label) | Lightslate.light.500:#6B7585Darkslate.dark.500:#C4CBD6 |
| Border Changes with theme when disabled | component.button.disabled.bordercolor.disabled.bordervar(--button-disabled-border) | Lightslate.light.200:#DDE2EADarkslate.dark.200:#3D4655 |
| Compact | ||
| Height | component.button.compact.heightsize.control.compactvar(--button-compact-height) | size.24:1.5rem |
| Padding, left and right | component.button.compact.padding-xspace.inset.cell-xvar(--button-compact-padding-x) | space.12:0.75rem |
| Large | ||
| Height | component.button.large.heightsize.control.largevar(--button-large-height) | size.32:2rem |
| Shared | ||
| Transition duration | component.transition.durationmotion.duration.feedbackvar(--transition-duration) | duration.fast:120ms |
| Transition easing | component.transition.easingmotion.easing.standardvar(--transition-easing) | easing.standard:cubic-bezier(0.2,0,0,1) |