Skip to content

role button

Icon button draft v0.5.1

Triggers an action with an icon alone; its label is given to assistive technology.

An icon button performs an action, like a button, but shows only an icon: it saves room in toolbars, table rows and headers where the action is familiar enough to be recognized from its symbol. It has no visible text, so it always carries a label that assistive technology announces as its name. Like every button, it never navigates.

Web implementation

Root .ds-icon-button on a <button>. Properties map to attributes: emphasis → data-emphasisintent → data-intentsize → data-sizelabel → aria-labeldisabled → disabled

packages/components/src/components/icon-button/icon-button.css
/*
 * Icon button — vanilla CSS reference implementation.
 * Spec:   packages/components/specs/components/icon-button.spec.json
 * Tokens: packages/tokens/dist/css/components/icon-button.css (generated from packages/tokens/src/components/icon-button.tokens.json)
 *
 * Rules:
 *  - Only --icon-button-* custom properties may appear here. No raw values, no semantic tokens.
 *  - Emphasis, intent and size are data attributes; states are native pseudo-classes.
 *  - Every state also matches [data-state="<state>"] so the docs site can force it for review.
 *  - The container is square: min-width equals min-height at every size and on every platform.
 *  - The icon is a ligature of the icon font (Material Symbols); the accessible name is the
 *    aria-label of the button, the glyph is aria-hidden.
 *  - 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-icon-button {
  /* Defaults = emphasis "accent", intent "neutral", size "medium", state "default". */
  --_background: var(--_background-default);
  --_border-color: var(--_border-color-default);
  --_color: var(--_color-default);
  --_min-height: var(--icon-button-container-min-height-medium);
  --_min-width: var(--icon-button-container-min-width-medium);
  --_border-radius: var(--icon-button-container-border-radius-medium);
  --_icon-size: var(--icon-button-icon-size-medium);

  box-sizing: border-box;
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  min-width: var(--_min-width);
  min-height: var(--_min-height);
  margin: 0;
  padding: 0;
  border: var(--icon-button-container-border-width) solid var(--_border-color);
  border-radius: var(--_border-radius);
  background-color: var(--_background);
  color: var(--_color);
  cursor: pointer;
  user-select: none;
  appearance: none;
  -webkit-appearance: none;
  transition:
    background-color var(--icon-button-container-transition-duration) var(--icon-button-container-transition-easing),
    border-color var(--icon-button-container-transition-duration) var(--icon-button-container-transition-easing),
    color var(--icon-button-container-transition-duration) var(--icon-button-container-transition-easing);
}

/* ---- Parts -------------------------------------------------------------- */

.ds-icon-button__icon {
  display: inline-block;
  flex: none;
  inline-size: var(--_icon-size);
  block-size: var(--_icon-size);
  overflow: hidden;
  font-family: var(--icon-button-icon-font-family);
  font-size: var(--_icon-size);
  font-weight: var(--icon-button-icon-font-weight);
  font-style: normal;
  font-variation-settings:
    "FILL" var(--icon-button-icon-fill),
    "GRAD" var(--icon-button-icon-grade),
    "opsz" var(--icon-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;
}

/* ---- Size ---------------------------------------------------------------- */

.ds-icon-button[data-size="xsmall"] {
  --_min-height: var(--icon-button-container-min-height-xsmall);
  --_min-width: var(--icon-button-container-min-width-xsmall);
  --_border-radius: var(--icon-button-container-border-radius-xsmall);
  --_icon-size: var(--icon-button-icon-size-xsmall);
}

.ds-icon-button[data-size="small"] {
  --_min-height: var(--icon-button-container-min-height-small);
  --_min-width: var(--icon-button-container-min-width-small);
  --_border-radius: var(--icon-button-container-border-radius-small);
  --_icon-size: var(--icon-button-icon-size-small);
}

.ds-icon-button[data-size="large"] {
  --_min-height: var(--icon-button-container-min-height-large);
  --_min-width: var(--icon-button-container-min-width-large);
  --_border-radius: var(--icon-button-container-border-radius-large);
  --_icon-size: var(--icon-button-icon-size-large);
}

/* ---- Emphasis x intent: color ladders ------------------------------------ */
/* Selectors are wrapped in :where() so every pair has the same specificity as .ds-icon-button. */

/* accent */
.ds-icon-button:where([data-emphasis="accent"], :not([data-emphasis])):where([data-intent="neutral"], :not([data-intent])) {
  --_background-default: var(--icon-button-container-background-accent-neutral-default);
  --_border-color-default: var(--icon-button-container-border-color-accent-neutral-default);
  --_color-default: var(--icon-button-icon-color-accent-neutral-default);
  --_background-hover: var(--icon-button-container-background-accent-neutral-hover);
  --_border-color-hover: var(--icon-button-container-border-color-accent-neutral-hover);
  --_color-hover: var(--icon-button-icon-color-accent-neutral-hover);
  --_background-active: var(--icon-button-container-background-accent-neutral-active);
  --_border-color-active: var(--icon-button-container-border-color-accent-neutral-active);
  --_color-active: var(--icon-button-icon-color-accent-neutral-active);
}
.ds-icon-button:where([data-emphasis="accent"], :not([data-emphasis])):where([data-intent="brand"]) {
  --_background-default: var(--icon-button-container-background-accent-brand-default);
  --_border-color-default: var(--icon-button-container-border-color-accent-brand-default);
  --_color-default: var(--icon-button-icon-color-accent-brand-default);
  --_background-hover: var(--icon-button-container-background-accent-brand-hover);
  --_border-color-hover: var(--icon-button-container-border-color-accent-brand-hover);
  --_color-hover: var(--icon-button-icon-color-accent-brand-hover);
  --_background-active: var(--icon-button-container-background-accent-brand-active);
  --_border-color-active: var(--icon-button-container-border-color-accent-brand-active);
  --_color-active: var(--icon-button-icon-color-accent-brand-active);
}
.ds-icon-button:where([data-emphasis="accent"], :not([data-emphasis])):where([data-intent="error"]) {
  --_background-default: var(--icon-button-container-background-accent-error-default);
  --_border-color-default: var(--icon-button-container-border-color-accent-error-default);
  --_color-default: var(--icon-button-icon-color-accent-error-default);
  --_background-hover: var(--icon-button-container-background-accent-error-hover);
  --_border-color-hover: var(--icon-button-container-border-color-accent-error-hover);
  --_color-hover: var(--icon-button-icon-color-accent-error-hover);
  --_background-active: var(--icon-button-container-background-accent-error-active);
  --_border-color-active: var(--icon-button-container-border-color-accent-error-active);
  --_color-active: var(--icon-button-icon-color-accent-error-active);
}
.ds-icon-button:where([data-emphasis="accent"], :not([data-emphasis])) {
  --_background-disabled: var(--icon-button-container-background-accent-disabled);
  --_border-color-disabled: var(--icon-button-container-border-color-accent-disabled);
  --_color-disabled: var(--icon-button-icon-color-accent-disabled);
}

/* subtle */
.ds-icon-button:where([data-emphasis="subtle"]):where([data-intent="neutral"], :not([data-intent])) {
  --_background-default: var(--icon-button-container-background-subtle-neutral-default);
  --_border-color-default: var(--icon-button-container-border-color-subtle-neutral-default);
  --_color-default: var(--icon-button-icon-color-subtle-neutral-default);
  --_background-hover: var(--icon-button-container-background-subtle-neutral-hover);
  --_border-color-hover: var(--icon-button-container-border-color-subtle-neutral-hover);
  --_color-hover: var(--icon-button-icon-color-subtle-neutral-hover);
  --_background-active: var(--icon-button-container-background-subtle-neutral-active);
  --_border-color-active: var(--icon-button-container-border-color-subtle-neutral-active);
  --_color-active: var(--icon-button-icon-color-subtle-neutral-active);
}
.ds-icon-button:where([data-emphasis="subtle"]):where([data-intent="brand"]) {
  --_background-default: var(--icon-button-container-background-subtle-brand-default);
  --_border-color-default: var(--icon-button-container-border-color-subtle-brand-default);
  --_color-default: var(--icon-button-icon-color-subtle-brand-default);
  --_background-hover: var(--icon-button-container-background-subtle-brand-hover);
  --_border-color-hover: var(--icon-button-container-border-color-subtle-brand-hover);
  --_color-hover: var(--icon-button-icon-color-subtle-brand-hover);
  --_background-active: var(--icon-button-container-background-subtle-brand-active);
  --_border-color-active: var(--icon-button-container-border-color-subtle-brand-active);
  --_color-active: var(--icon-button-icon-color-subtle-brand-active);
}
.ds-icon-button:where([data-emphasis="subtle"]):where([data-intent="error"]) {
  --_background-default: var(--icon-button-container-background-subtle-error-default);
  --_border-color-default: var(--icon-button-container-border-color-subtle-error-default);
  --_color-default: var(--icon-button-icon-color-subtle-error-default);
  --_background-hover: var(--icon-button-container-background-subtle-error-hover);
  --_border-color-hover: var(--icon-button-container-border-color-subtle-error-hover);
  --_color-hover: var(--icon-button-icon-color-subtle-error-hover);
  --_background-active: var(--icon-button-container-background-subtle-error-active);
  --_border-color-active: var(--icon-button-container-border-color-subtle-error-active);
  --_color-active: var(--icon-button-icon-color-subtle-error-active);
}
.ds-icon-button:where([data-emphasis="subtle"]) {
  --_background-disabled: var(--icon-button-container-background-subtle-disabled);
  --_border-color-disabled: var(--icon-button-container-border-color-subtle-disabled);
  --_color-disabled: var(--icon-button-icon-color-subtle-disabled);
}

/* ghost */
.ds-icon-button:where([data-emphasis="ghost"]):where([data-intent="neutral"], :not([data-intent])) {
  --_background-default: var(--icon-button-container-background-ghost-neutral-default);
  --_border-color-default: var(--icon-button-container-border-color-ghost-neutral-default);
  --_color-default: var(--icon-button-icon-color-ghost-neutral-default);
  --_background-hover: var(--icon-button-container-background-ghost-neutral-hover);
  --_border-color-hover: var(--icon-button-container-border-color-ghost-neutral-hover);
  --_color-hover: var(--icon-button-icon-color-ghost-neutral-hover);
  --_background-active: var(--icon-button-container-background-ghost-neutral-active);
  --_border-color-active: var(--icon-button-container-border-color-ghost-neutral-active);
  --_color-active: var(--icon-button-icon-color-ghost-neutral-active);
}
.ds-icon-button:where([data-emphasis="ghost"]):where([data-intent="brand"]) {
  --_background-default: var(--icon-button-container-background-ghost-brand-default);
  --_border-color-default: var(--icon-button-container-border-color-ghost-brand-default);
  --_color-default: var(--icon-button-icon-color-ghost-brand-default);
  --_background-hover: var(--icon-button-container-background-ghost-brand-hover);
  --_border-color-hover: var(--icon-button-container-border-color-ghost-brand-hover);
  --_color-hover: var(--icon-button-icon-color-ghost-brand-hover);
  --_background-active: var(--icon-button-container-background-ghost-brand-active);
  --_border-color-active: var(--icon-button-container-border-color-ghost-brand-active);
  --_color-active: var(--icon-button-icon-color-ghost-brand-active);
}
.ds-icon-button:where([data-emphasis="ghost"]):where([data-intent="error"]) {
  --_background-default: var(--icon-button-container-background-ghost-error-default);
  --_border-color-default: var(--icon-button-container-border-color-ghost-error-default);
  --_color-default: var(--icon-button-icon-color-ghost-error-default);
  --_background-hover: var(--icon-button-container-background-ghost-error-hover);
  --_border-color-hover: var(--icon-button-container-border-color-ghost-error-hover);
  --_color-hover: var(--icon-button-icon-color-ghost-error-hover);
  --_background-active: var(--icon-button-container-background-ghost-error-active);
  --_border-color-active: var(--icon-button-container-border-color-ghost-error-active);
  --_color-active: var(--icon-button-icon-color-ghost-error-active);
}
.ds-icon-button:where([data-emphasis="ghost"]) {
  --_background-disabled: var(--icon-button-container-background-ghost-disabled);
  --_border-color-disabled: var(--icon-button-container-border-color-ghost-disabled);
  --_color-disabled: var(--icon-button-icon-color-ghost-disabled);
}

/* ---- States -------------------------------------------------------------- */
/* Order matters: default < hover < active < disabled. Guards keep hover/active off disabled buttons. */

.ds-icon-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-icon-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-icon-button:where(:disabled, [data-state="disabled"]) {
  --_background: var(--_background-disabled);
  --_border-color: var(--_border-color-disabled);
  --_color: var(--_color-disabled);
  cursor: not-allowed;
}

.ds-icon-button:focus {
  outline: none;
}

.ds-icon-button:is(:focus-visible, [data-state="focus-visible"]) {
  outline: var(--icon-button-container-outline-width-focus-visible) solid var(--icon-button-container-outline-color-focus-visible);
  outline-offset: var(--icon-button-container-outline-offset-focus-visible);
}

@media (prefers-reduced-motion: reduce) {
  .ds-icon-button {
    transition: none;
  }
}
packages/components/src/components/icon-button/icon-button.template.mjs
/**
 * Icon button — HTML template for the web implementation.
 * Pure function of the spec properties (packages/components/specs/components/icon-button.spec.json) -> HTML string.
 * Used by the docs site (server and client side). No DOM, no framework.
 *
 * The label is the accessible name, given as aria-label; the glyph is aria-hidden so its
 * ligature name ("delete") is never read.
 * Extra sandbox-only prop: `state` forces a visual state through data-state.
 */
import { escapeAttr, renderIcon, ICONS } from '../../lib/template-utils.mjs';

export const meta = {
  id: 'icon-button',
  selector: '.ds-icon-button',
  icons: ICONS,
};

export function render(props = {}) {
  const {
    emphasis = 'accent',
    intent = 'neutral',
    size = 'medium',
    icon = 'add',
    label = 'Add',
    disabled = false,
    state = null,
    type = 'button',
  } = props;

  const attrs = [
    `type="${escapeAttr(type)}"`,
    'class="ds-icon-button"',
    `data-emphasis="${escapeAttr(emphasis)}"`,
    `data-intent="${escapeAttr(intent)}"`,
    `data-size="${escapeAttr(size)}"`,
    `aria-label="${escapeAttr(label)}"`,
  ];
  if (disabled) attrs.push('disabled');
  if (state && state !== 'default') attrs.push(`data-state="${escapeAttr(state)}"`);

  const glyph = `<span class="ds-icon-button__icon" data-part="icon" aria-hidden="true">${renderIcon(icon || 'add')}</span>`;
  return `<button ${attrs.join(' ')}>${glyph}</button>`;
}

No other platform implementation recorded yet. Platform token outputs live under Platforms.