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">.