Button

button.btn[type="button"]

Button — the reference implementation.

Examples

The six variants

View “The six variants” as
<button class="btn btn-primary" type="button">Save changes</button>
<button class="btn btn-secondary" type="button">Preview</button>
<button class="btn btn-accent" type="button">Publish</button>
<button class="btn btn-outline" type="button">Cancel</button>
<button class="btn btn-quiet" type="button">Dismiss</button>
<button class="btn btn-danger" type="button">Delete</button>

With an icon

View “With an icon” as
<button class="btn btn-primary" type="button">
  <svg class="icon" aria-hidden="true"><use href="#icon-plus"></use></svg>
  New invoice
</button>
<button class="btn btn-outline" type="button">
  Export
  <svg class="icon" aria-hidden="true"><use href="#icon-external-link"></use></svg>
</button>
<button class="btn btn-danger btn-sm" type="button">
  <svg class="icon" aria-hidden="true"><use href="#icon-trash"></use></svg>
  Delete
</button>

Put the <svg> inside the button and nothing else is needed: .btn is a flex row with a gap, so a mark before or after the label lines up on its own and the icon scales with the button’s size.

The glyph is always aria-hidden="true" — the words beside it are the name, and an icon that also announced itself would say the same thing twice. A mark before the label describes what the action creates; a mark after it says where the action takes you, which is why the external-link arrow is on the trailing side. Where there is no label at all, use .btn-icon and put the name on the button, as in the next example.

Sizes, and the shapes

View “Sizes, and the shapes” as
<button class="btn btn-primary btn-sm" type="button">Small</button>
<button class="btn btn-primary" type="button">Default</button>
<button class="btn btn-primary btn-lg" type="button">Large</button>
<button class="btn btn-outline btn-round" type="button">Round</button>
<button class="btn btn-outline btn-icon" type="button" aria-label="Search">
  <svg class="icon" aria-hidden="true"><use href="#icon-search"></use></svg>
</button>

The icon-only button carries its name on the button, never on the <svg>.

State the platform already models

View “State the platform already models” as
<button class="btn btn-primary" type="button" disabled>Disabled</button>
<button class="btn btn-outline" type="button" aria-pressed="true">Pressed</button>
<button class="btn btn-primary" type="button" data-loading aria-busy="true" disabled>
  Saving
</button>
<div class="btn-group" role="group" aria-label="Text alignment">
  <button class="btn" type="button" aria-pressed="true">Left</button>
  <button class="btn" type="button" aria-pressed="false">Centre</button>
  <button class="btn" type="button" aria-pressed="false">Right</button>
</div>

:disabled and [aria-pressed] are read straight from the element. data-loading is the exception — the platform has no notion of it.

A default action with its own menu

View “A default action with its own menu” as
<div class="btn-group btn-split">
  <button class="btn btn-primary" type="button">Save</button>
  <button class="btn btn-primary btn-split-menu" type="button"
          command="toggle-popover" commandfor="save-menu">
    <span class="sr-only">More save options</span>
  </button>
</div>
<div class="menu popover popover-plain" id="save-menu" popover role="menu"
     aria-label="More save options" data-ui="menu">
  <button class="menu-item" type="button" role="menuitem">Save a copy</button>
  <button class="menu-item" type="button" role="menuitem">Save as template</button>
</div>

data-ui="menu" goes on the panel and nowhere else — it is what writes aria-expanded back onto the button, since an invoker command supplies none.

Putting a second data-ui="popover" on the button is the mistake this example shipped with, and the module says so in the console: the attribute names an element that has an open state, and a button does not have one. This was a component of its own called split-button. It is not one: it is a .btn-group holding two .btns, and every variant, size and state already applies to both halves — so the classes moved here and took the button prefix. The chevron is drawn in CSS rather than asked of you, because an <svg> here is a decorative element somebody has to remember to hide and forgetting is how "chevron down" becomes a button's name. The two halves are two Tab stops, deliberately: they are two buttons.

Tokens 17

Level 2, declared on .btn itself. Set any of them on that selector to restyle this component without touching the skin.

Level 2 tokens declared by button
TokenDefault
--btn-split-chevron-size0.375rem
--btn-split-chevron-thicknessvar(--border-width-strong)
--btn-split-divider-opacity0.32
--btn-split-menu-padding-inlinevar(--space-2)
--btn-active-scale0.97
--btn-bgtransparent
--btn-border-colorvar(--color-border-strong)
--btn-border-widthvar(--border-width)
--btn-colorvar(--color-on-surface)
--btn-font-familyvar(--font-secondary)
--btn-font-sizevar(--font-size-sm)
--btn-font-weightvar(--font-weight-medium)
--btn-gapvar(--space-2)
--btn-min-sizevar(--target-size)
--btn-padding-blockvar(--space-2)
--btn-padding-inlinevar(--space-4)
--btn-radiusvar(--radius-field)

Variants and states

Variants

  • .btn-accent
  • .btn-primary
  • .btn-secondary
  • .btn-outline
  • .btn-quiet
  • .btn-danger
  • .btn-lg
  • .btn-round
  • .btn-sm
  • .btn-split

Inside it

  • .btn-group
  • .btn-split-menu
  • .btn-icon

State it reads

Read from the platform, never mirrored into a class that could disagree with it.

  • :active
  • :disabled
  • :focus
  • :hover
  • data-loading

Before you ship it

What you have to do 1 requirements

  1. An icon-only button has no accessible name. Give it one on the buttonaria-label, or <span class="sr-only"> inside — never on the <svg>, which is aria-hidden. Nothing in CSS can supply this and nothing can detect its absence at runtime, so it is on you.

src/css/components/button.css · npx mostlycss add button