Skip to content

Button

A button built on the native <button>, styled through two attributes - a variant for the shape and a color for the hue.

Usage

<button is="root-button" type="button" data-variant="primary" data-color="default">Save</button>

data-variant and data-color work as a pair, and neither is optional in practice: the variant decides which properties take the color - background, border, text - and data-color is what supplies it. A button with a variant and no color paints nothing, because the color base it reads is undefined; the result is the bare outline below, which is also what a button with no attributes at all looks like.

<button is="root-button" type="button">No variant, no color</button>

Variants

Three shapes over the same color. Measured on data-color="default": primary fills with the ramp step and takes a contrast-color() label, secondary keeps the border and the text in the color, tertiary keeps only the text.

<button is="root-button" type="button" data-variant="primary" data-color="default">Primary</button>
<button is="root-button" type="button" data-variant="secondary" data-color="default">Secondary</button>
<button is="root-button" type="button" data-variant="tertiary" data-color="default">Tertiary</button>

Palettes

data-color names a ramp: default (blue), danger (red) and neutral. The value read is that ramp's -400 step, and it also tints the focus ring through --root-f--outline-color.

<button is="root-button" type="button" data-variant="primary" data-color="default">Default</button>
<button is="root-button" type="button" data-variant="primary" data-color="danger">Danger</button>
<button is="root-button" type="button" data-variant="primary" data-color="neutral">Neutral</button>

Every variant takes every palette:

<button is="root-button" type="button" data-variant="secondary" data-color="danger">Secondary danger</button>
<button is="root-button" type="button" data-variant="tertiary" data-color="danger">Tertiary danger</button>
<button is="root-button" type="button" data-variant="secondary" data-color="neutral">Secondary neutral</button>
<button is="root-button" type="button" data-variant="tertiary" data-color="neutral">Tertiary neutral</button>

Sizes

data-size takes small and large; the default is the medium step and has no value to write. Measured heights are 36, 40 and 44px, and the font weight moves with the size - 400, 500, 700 - while large also takes the next radius step.

<button is="root-button" type="button" data-variant="primary" data-color="default" data-size="large">Large</button>
<button is="root-button" type="button" data-variant="primary" data-color="default">Medium</button>
<button is="root-button" type="button" data-variant="primary" data-color="default" data-size="small">Small</button>

Modifiers

square and circle both force a 1:1 aspect ratio, so the button sizes off its height and takes an icon rather than a label; circle also takes the full radius. An icon-only button needs an accessible name of its own - aria-label.

<button is="root-button" type="button" data-variant="primary" data-color="default" square aria-label="Copy">
  <i is="root-icon" icon="clipboard"></i>
</button>
<button is="root-button" type="button" data-variant="primary" data-color="default" circle aria-label="Confirm">
  <i is="root-icon" icon="check"></i>
</button>

States

Hover, :active and [aria-selected="true"] move the surface deeper into its own ramp: one step for selected, two for hover, three for active - 12 points of lightness each in the light scheme, 4 in the dark one, which is exactly the distance between two ramp steps. The label keeps the color its variant derived, so the contrast improves as the surface darkens (measured on primary/default: 5.8:1 at rest, 8.7:1 selected, 11.9:1 hover, 15.7:1 active).

aria-selected is the one that is authored; hover and active are the pointer's.

<button is="root-button" type="button" data-variant="primary" data-color="default" aria-selected="true">Selected</button>
<button is="root-button" type="button" data-variant="secondary" data-color="default" aria-selected="true">Selected secondary</button>

disabled is the native attribute: it stops the click, drops the button out of the tab order and fires root-button.disabled.change. It carries no root styling yet - the disabled branch in _states.css is still open with design.

<button is="root-button" type="button" data-variant="primary" data-color="default" aria-disabled="true">Disabled</button>
<button is="root-button" type="button" data-variant="secondary" data-color="default" aria-disabled="true">Disabled</button>
<button is="root-button" type="button" data-variant="tertiary" data-color="default" aria-disabled="true">Disabled</button>

Focus

The ring belongs to foundations/_focus.css alone; Button declares no outline-* of its own, which is what makes the ring show up at all. The palette tints it: a danger button focuses red.

<button is="root-button" type="button" data-variant="primary" data-color="danger">Tab to me</button>

Segmented group

A segmented control is a row of buttons with one of them pressed, so it is a row of buttons: role="group" with a label on the row, aria-pressed on each button, and the pressed one wearing primary while the rest wear tertiary. There is no component for this - what a component would hold is exactly what the markup already says.

<div role="group" aria-label="View mode" data-segmented>
  <button is="root-button" type="button" data-variant="primary" data-color="default" aria-pressed="true">Day</button>
  <button is="root-button" type="button" data-variant="tertiary" data-color="default" aria-pressed="false">Week</button>
  <button is="root-button" type="button" data-variant="tertiary" data-color="default" aria-pressed="false">Month</button>
</div>

One press at a time is four lines on the group:

group.addEventListener('click', (event) => {
  const pressed = event.target.closest('button[aria-pressed]');

  if (!pressed) return;

  for (const button of group.querySelectorAll('button[aria-pressed]')) {
    const isPressed = button === pressed;

    button.setAttribute('aria-pressed', String(isPressed));
    button.dataset.variant = isPressed ? 'primary' : 'tertiary';
  }
});

Attributes

Attribute Type Default Description
type 'button' | 'submit' | 'reset' 'button' Set by the component when absent
data-variant 'primary' | 'secondary' | 'tertiary' - Which properties take the color
data-color 'default' | 'danger' | 'neutral' - Which ramp supplies it
data-size 'small' | 'large' medium Height, weight and radius step
square boolean false 1:1 aspect ratio
circle boolean false 1:1 aspect ratio and a full radius
aria-selected 'true' - Selected state, one ramp step deeper
disabled boolean false Native disabled
aria-label string - Accessible name, required for an icon-only button

Events

Namespaced as <component>.<event>, bubbling and cancelable; detail also carries the originating DOM event as originalEvent.

Event Detail Description
root-button.click { value } Fired on click
root-button.focus { value } Fired on focus
root-button.disabled.change { disabled } Fired when the disabled state changes

Extends

HTMLButtonElement - use with <button is="root-button">.