role button
Button draft v0.22.1
Triggers an action or event when activated.
A button lets people perform an action with a single activation: submit a form, open a dialog, confirm a choice. Its emphasis communicates how important the action is on the screen; its intent communicates what kind of action it is. Buttons never navigate; use a link for navigation.
Web implementation
Root .ds-button on a <button>. Properties map to attributes: emphasis → data-emphasisintent → data-intentsize → data-sizedisabled → disabled
packages/components/src/components/button/button.css
/*
* Button — vanilla CSS reference implementation.
* Spec: packages/components/specs/components/button.spec.json
* Tokens: packages/tokens/dist/css/components/button.css (generated from packages/tokens/src/components/button.tokens.json)
*
* Rules:
* - Only --button-* custom properties may appear here. No raw values, no semantic tokens.
* - Emphasis, intent and size are data attributes; states are native pseudo-classes.
* - Icons are ligatures of the icon font (Material Symbols); the glyph is the icon size square.
* - Every state also matches [data-state="<state>"] so the docs site can force it for review.
* - Private properties (--_*) are the single place an option or a state writes to:
* each emphasis x intent pair sets its ladder (--_<property>-<state>), each emphasis its
* disabled colors, and the state rules pick the rung.
*/
.ds-button {
/* Defaults = emphasis "accent", intent "neutral", size "medium", state "default". */
--_background: var(--_background-default);
--_border-color: var(--_border-color-default);
--_color: var(--_color-default);
--_padding-inline: var(--button-container-padding-inline-medium);
--_padding-block: var(--button-container-padding-block-medium);
--_gap: var(--button-container-gap-medium);
--_padding-inline-icon: var(--button-container-padding-inline-icon-side-medium);
--_padding-inline-label: var(--button-container-padding-inline-label-side-medium);
--_min-height: var(--button-container-min-height-medium);
--_border-radius: var(--button-container-border-radius-medium);
--_icon-size: var(--button-icon-size-medium);
--_font-family: var(--button-label-typography-medium-font-family);
--_font-size: var(--button-label-typography-medium-font-size);
--_font-weight: var(--button-label-typography-medium-font-weight);
--_line-height: var(--button-label-typography-medium-line-height);
--_letter-spacing: var(--button-label-typography-medium-letter-spacing);
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--_gap);
min-height: var(--_min-height);
margin: 0;
padding-block: var(--_padding-block);
padding-inline: var(--_padding-inline);
border: var(--button-container-border-width) solid var(--_border-color);
border-radius: var(--_border-radius);
background-color: var(--_background);
color: var(--_color);
font-family: var(--_font-family);
font-size: var(--_font-size);
font-weight: var(--_font-weight);
line-height: var(--_line-height);
letter-spacing: var(--_letter-spacing);
text-decoration: none;
white-space: nowrap;
cursor: pointer;
user-select: none;
appearance: none;
-webkit-appearance: none;
transition:
background-color var(--button-container-transition-duration) var(--button-container-transition-easing),
border-color var(--button-container-transition-duration) var(--button-container-transition-easing),
color var(--button-container-transition-duration) var(--button-container-transition-easing);
}
/* ---- Parts -------------------------------------------------------------- */
.ds-button__label {
display: inline-block;
}
.ds-button__icon {
display: inline-block;
flex: none;
inline-size: var(--_icon-size);
block-size: var(--_icon-size);
overflow: hidden;
font-family: var(--button-icon-font-family);
font-size: var(--_icon-size);
font-weight: var(--button-icon-font-weight);
font-style: normal;
font-variation-settings:
"FILL" var(--button-icon-fill),
"GRAD" var(--button-icon-grade),
"opsz" var(--button-icon-optical-size);
font-feature-settings: "liga";
line-height: var(--_icon-size);
letter-spacing: normal;
text-transform: none;
white-space: nowrap;
overflow-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased;
}
/* ---- Padding next to an icon --------------------------------------------- */
/* With an icon, the label side takes the label-side padding and each icon side the icon-side padding. */
.ds-button:has(> .ds-button__icon) {
padding-inline: var(--_padding-inline-label);
}
.ds-button:has(> .ds-button__icon[data-position="start"]) {
padding-inline-start: var(--_padding-inline-icon);
}
.ds-button:has(> .ds-button__icon[data-position="end"]) {
padding-inline-end: var(--_padding-inline-icon);
}
/* ---- Size ---------------------------------------------------------------- */
.ds-button[data-size="xsmall"] {
--_padding-inline: var(--button-container-padding-inline-xsmall);
--_padding-block: var(--button-container-padding-block-xsmall);
--_gap: var(--button-container-gap-xsmall);
--_padding-inline-icon: var(--button-container-padding-inline-icon-side-xsmall);
--_padding-inline-label: var(--button-container-padding-inline-label-side-xsmall);
--_min-height: var(--button-container-min-height-xsmall);
--_border-radius: var(--button-container-border-radius-xsmall);
--_icon-size: var(--button-icon-size-xsmall);
--_font-family: var(--button-label-typography-xsmall-font-family);
--_font-size: var(--button-label-typography-xsmall-font-size);
--_font-weight: var(--button-label-typography-xsmall-font-weight);
--_line-height: var(--button-label-typography-xsmall-line-height);
--_letter-spacing: var(--button-label-typography-xsmall-letter-spacing);
}
.ds-button[data-size="small"] {
--_padding-inline: var(--button-container-padding-inline-small);
--_padding-block: var(--button-container-padding-block-small);
--_gap: var(--button-container-gap-small);
--_padding-inline-icon: var(--button-container-padding-inline-icon-side-small);
--_padding-inline-label: var(--button-container-padding-inline-label-side-small);
--_min-height: var(--button-container-min-height-small);
--_border-radius: var(--button-container-border-radius-small);
--_icon-size: var(--button-icon-size-small);
--_font-family: var(--button-label-typography-small-font-family);
--_font-size: var(--button-label-typography-small-font-size);
--_font-weight: var(--button-label-typography-small-font-weight);
--_line-height: var(--button-label-typography-small-line-height);
--_letter-spacing: var(--button-label-typography-small-letter-spacing);
}
.ds-button[data-size="large"] {
--_padding-inline: var(--button-container-padding-inline-large);
--_padding-block: var(--button-container-padding-block-large);
--_gap: var(--button-container-gap-large);
--_padding-inline-icon: var(--button-container-padding-inline-icon-side-large);
--_padding-inline-label: var(--button-container-padding-inline-label-side-large);
--_min-height: var(--button-container-min-height-large);
--_border-radius: var(--button-container-border-radius-large);
--_icon-size: var(--button-icon-size-large);
--_font-family: var(--button-label-typography-large-font-family);
--_font-size: var(--button-label-typography-large-font-size);
--_font-weight: var(--button-label-typography-large-font-weight);
--_line-height: var(--button-label-typography-large-line-height);
--_letter-spacing: var(--button-label-typography-large-letter-spacing);
}
/* ---- Emphasis x intent: color ladders ------------------------------------ */
/* Selectors are wrapped in :where() so every pair has the same specificity as .ds-button. */
/* accent */
.ds-button:where([data-emphasis="accent"], :not([data-emphasis])):where([data-intent="neutral"], :not([data-intent])) {
--_background-default: var(--button-container-background-accent-neutral-default);
--_border-color-default: var(--button-container-border-color-accent-neutral-default);
--_color-default: var(--button-label-color-accent-neutral-default);
--_background-hover: var(--button-container-background-accent-neutral-hover);
--_border-color-hover: var(--button-container-border-color-accent-neutral-hover);
--_color-hover: var(--button-label-color-accent-neutral-hover);
--_background-active: var(--button-container-background-accent-neutral-active);
--_border-color-active: var(--button-container-border-color-accent-neutral-active);
--_color-active: var(--button-label-color-accent-neutral-active);
}
.ds-button:where([data-emphasis="accent"], :not([data-emphasis])):where([data-intent="brand"]) {
--_background-default: var(--button-container-background-accent-brand-default);
--_border-color-default: var(--button-container-border-color-accent-brand-default);
--_color-default: var(--button-label-color-accent-brand-default);
--_background-hover: var(--button-container-background-accent-brand-hover);
--_border-color-hover: var(--button-container-border-color-accent-brand-hover);
--_color-hover: var(--button-label-color-accent-brand-hover);
--_background-active: var(--button-container-background-accent-brand-active);
--_border-color-active: var(--button-container-border-color-accent-brand-active);
--_color-active: var(--button-label-color-accent-brand-active);
}
.ds-button:where([data-emphasis="accent"], :not([data-emphasis])):where([data-intent="error"]) {
--_background-default: var(--button-container-background-accent-error-default);
--_border-color-default: var(--button-container-border-color-accent-error-default);
--_color-default: var(--button-label-color-accent-error-default);
--_background-hover: var(--button-container-background-accent-error-hover);
--_border-color-hover: var(--button-container-border-color-accent-error-hover);
--_color-hover: var(--button-label-color-accent-error-hover);
--_background-active: var(--button-container-background-accent-error-active);
--_border-color-active: var(--button-container-border-color-accent-error-active);
--_color-active: var(--button-label-color-accent-error-active);
}
.ds-button:where([data-emphasis="accent"], :not([data-emphasis])) {
--_background-disabled: var(--button-container-background-accent-disabled);
--_border-color-disabled: var(--button-container-border-color-accent-disabled);
--_color-disabled: var(--button-label-color-accent-disabled);
}
/* subtle */
.ds-button:where([data-emphasis="subtle"]):where([data-intent="neutral"], :not([data-intent])) {
--_background-default: var(--button-container-background-subtle-neutral-default);
--_border-color-default: var(--button-container-border-color-subtle-neutral-default);
--_color-default: var(--button-label-color-subtle-neutral-default);
--_background-hover: var(--button-container-background-subtle-neutral-hover);
--_border-color-hover: var(--button-container-border-color-subtle-neutral-hover);
--_color-hover: var(--button-label-color-subtle-neutral-hover);
--_background-active: var(--button-container-background-subtle-neutral-active);
--_border-color-active: var(--button-container-border-color-subtle-neutral-active);
--_color-active: var(--button-label-color-subtle-neutral-active);
}
.ds-button:where([data-emphasis="subtle"]):where([data-intent="brand"]) {
--_background-default: var(--button-container-background-subtle-brand-default);
--_border-color-default: var(--button-container-border-color-subtle-brand-default);
--_color-default: var(--button-label-color-subtle-brand-default);
--_background-hover: var(--button-container-background-subtle-brand-hover);
--_border-color-hover: var(--button-container-border-color-subtle-brand-hover);
--_color-hover: var(--button-label-color-subtle-brand-hover);
--_background-active: var(--button-container-background-subtle-brand-active);
--_border-color-active: var(--button-container-border-color-subtle-brand-active);
--_color-active: var(--button-label-color-subtle-brand-active);
}
.ds-button:where([data-emphasis="subtle"]):where([data-intent="error"]) {
--_background-default: var(--button-container-background-subtle-error-default);
--_border-color-default: var(--button-container-border-color-subtle-error-default);
--_color-default: var(--button-label-color-subtle-error-default);
--_background-hover: var(--button-container-background-subtle-error-hover);
--_border-color-hover: var(--button-container-border-color-subtle-error-hover);
--_color-hover: var(--button-label-color-subtle-error-hover);
--_background-active: var(--button-container-background-subtle-error-active);
--_border-color-active: var(--button-container-border-color-subtle-error-active);
--_color-active: var(--button-label-color-subtle-error-active);
}
.ds-button:where([data-emphasis="subtle"]) {
--_background-disabled: var(--button-container-background-subtle-disabled);
--_border-color-disabled: var(--button-container-border-color-subtle-disabled);
--_color-disabled: var(--button-label-color-subtle-disabled);
}
/* ghost */
.ds-button:where([data-emphasis="ghost"]):where([data-intent="neutral"], :not([data-intent])) {
--_background-default: var(--button-container-background-ghost-neutral-default);
--_border-color-default: var(--button-container-border-color-ghost-neutral-default);
--_color-default: var(--button-label-color-ghost-neutral-default);
--_background-hover: var(--button-container-background-ghost-neutral-hover);
--_border-color-hover: var(--button-container-border-color-ghost-neutral-hover);
--_color-hover: var(--button-label-color-ghost-neutral-hover);
--_background-active: var(--button-container-background-ghost-neutral-active);
--_border-color-active: var(--button-container-border-color-ghost-neutral-active);
--_color-active: var(--button-label-color-ghost-neutral-active);
}
.ds-button:where([data-emphasis="ghost"]):where([data-intent="brand"]) {
--_background-default: var(--button-container-background-ghost-brand-default);
--_border-color-default: var(--button-container-border-color-ghost-brand-default);
--_color-default: var(--button-label-color-ghost-brand-default);
--_background-hover: var(--button-container-background-ghost-brand-hover);
--_border-color-hover: var(--button-container-border-color-ghost-brand-hover);
--_color-hover: var(--button-label-color-ghost-brand-hover);
--_background-active: var(--button-container-background-ghost-brand-active);
--_border-color-active: var(--button-container-border-color-ghost-brand-active);
--_color-active: var(--button-label-color-ghost-brand-active);
}
.ds-button:where([data-emphasis="ghost"]):where([data-intent="error"]) {
--_background-default: var(--button-container-background-ghost-error-default);
--_border-color-default: var(--button-container-border-color-ghost-error-default);
--_color-default: var(--button-label-color-ghost-error-default);
--_background-hover: var(--button-container-background-ghost-error-hover);
--_border-color-hover: var(--button-container-border-color-ghost-error-hover);
--_color-hover: var(--button-label-color-ghost-error-hover);
--_background-active: var(--button-container-background-ghost-error-active);
--_border-color-active: var(--button-container-border-color-ghost-error-active);
--_color-active: var(--button-label-color-ghost-error-active);
}
.ds-button:where([data-emphasis="ghost"]) {
--_background-disabled: var(--button-container-background-ghost-disabled);
--_border-color-disabled: var(--button-container-border-color-ghost-disabled);
--_color-disabled: var(--button-label-color-ghost-disabled);
}
/* ---- States -------------------------------------------------------------- */
/* Order matters: default < hover < active < disabled. Guards keep hover/active off disabled buttons. */
.ds-button:where(:hover, [data-state="hover"]):not(:disabled, [data-state="disabled"]) {
--_background: var(--_background-hover);
--_border-color: var(--_border-color-hover);
--_color: var(--_color-hover);
}
.ds-button:where(:active, [data-state="active"]):not(:disabled, [data-state="disabled"]) {
--_background: var(--_background-active);
--_border-color: var(--_border-color-active);
--_color: var(--_color-active);
}
.ds-button:where(:disabled, [data-state="disabled"]) {
--_background: var(--_background-disabled);
--_border-color: var(--_border-color-disabled);
--_color: var(--_color-disabled);
cursor: not-allowed;
}
.ds-button:focus {
outline: none;
}
.ds-button:is(:focus-visible, [data-state="focus-visible"]) {
outline: var(--button-container-outline-width-focus-visible) solid var(--button-container-outline-color-focus-visible);
outline-offset: var(--button-container-outline-offset-focus-visible);
}
@media (prefers-reduced-motion: reduce) {
.ds-button {
transition: none;
}
}
packages/components/src/components/button/button.template.mjs
/**
* Button — HTML template for the web implementation.
* Pure function of the spec properties (packages/components/specs/components/button.spec.json) -> HTML string.
* Used by the docs site (server and client side). No DOM, no framework.
*
* Extra sandbox-only prop: `state` forces a visual state through data-state.
* Extra composition prop: `attrs` adds HTML attributes to the button (a `class` is appended to
* ds-button), so a component built on Button (Dropdown) can wire it without copying its markup.
*/
import { escapeHtml, escapeAttr, renderIcon, ICONS } from '../../lib/template-utils.mjs';
export const meta = {
id: 'button',
selector: '.ds-button',
icons: ICONS,
};
export function render(props = {}) {
const {
emphasis = 'accent',
intent = 'neutral',
size = 'medium',
label = 'Button',
iconStart = null,
iconEnd = null,
disabled = false,
state = null,
type = 'button',
attrs: extra = {},
} = props;
const { class: extraClass, ...extraAttrs } = extra;
const attrs = [
`type="${escapeAttr(type)}"`,
`class="${['ds-button', extraClass].filter(Boolean).map(escapeAttr).join(' ')}"`,
`data-emphasis="${escapeAttr(emphasis)}"`,
`data-intent="${escapeAttr(intent)}"`,
`data-size="${escapeAttr(size)}"`,
];
if (disabled) attrs.push('disabled');
if (state && state !== 'default') attrs.push(`data-state="${escapeAttr(state)}"`);
for (const [name, value] of Object.entries(extraAttrs)) {
if (value === false || value == null) continue;
attrs.push(value === true ? escapeAttr(name) : `${escapeAttr(name)}="${escapeAttr(value)}"`);
}
const parts = [];
if (iconStart) parts.push(`<span class="ds-button__icon" data-part="icon" data-position="start" aria-hidden="true">${renderIcon(iconStart)}</span>`);
parts.push(`<span class="ds-button__label" data-part="label">${escapeHtml(label)}</span>`);
if (iconEnd) parts.push(`<span class="ds-button__icon" data-part="icon" data-position="end" aria-hidden="true">${renderIcon(iconEnd)}</span>`);
return `<button ${attrs.join(' ')}>${parts.join('')}</button>`;
}
No other platform implementation recorded yet. Platform token outputs live under Platforms.