Skip to main content

Core · Actions and input

Button

Starts an action on the current page, such as saving, sending or opening a dialog.

Example

The five variants, the three sizes, buttons with an icon, a disabled button and a link that looks like a button.

Next step
Link as button

Known issues

None known.

Code

Copy the markup

A bare button needs no class. With no type, or with type="submit", it is the primary action; type="button" is the secondary one:

HTML
<button type="submit">Save changes</button>
<button type="button">Cancel</button>

.luz-btn adds what HTML cannot say: the look of a button on an a, the roles, the sizes and the full width. With the class:

HTML
<button class="luz-btn luz-btn--primary" type="button">Save changes</button>

A link that goes somewhere, with the look of a button:

HTML
<a class="luz-btn luz-btn--outline" href="/pricing/">See the plans</a>

The full width of a narrow container, such as a card or a form on a phone:

HTML
<form action="/cart/add" method="post">
  <button class="luz-btn luz-btn--primary luz-btn--block" type="submit">Add to cart</button>
</form>

With an icon, before or after the label, or alone. Take the icon from Icons, or draw your own on the same 24 × 24 grid:

HTML
<button class="luz-btn luz-btn--primary" type="submit">
  <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 4h2.5l2.2 10.5h9.6L19.5 7H6.2"/><circle cx="9" cy="19" r="1.4"/><circle cx="17" cy="19" r="1.4"/></svg>
  Add to cart
</button>

<!-- After the label: an icon that points to the end of the line carries data-luz-flip -->
<a class="luz-btn luz-btn--outline" href="/checkout/">
  Next step
  <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" data-luz-flip=""><path d="M5 12h14M13 6l6 6-6 6"/></svg>
</a>

<!-- Icon only: the name is real text, hidden from the eye -->
<button class="luz-btn luz-btn--ghost" type="button">
  <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M12 5v14M5 12h14"/></svg>
  <span class="u-sr-only">Add</span>
</button>

Variants and options

Combine .luz-btn with one variant and, if needed, one size and the full width.

Class or attributeEffect
.luz-btn--primary
Brand fill: the main action of the view
.luz-btn--secondary
Strong neutral surface: a second action
.luz-btn--outline
Brand text and border, no fill
.luz-btn--ghost
Brand text only: a low-priority action
.luz-btn--danger
Danger fill: an action that deletes or cannot be undone
.luz-btn--sm
36 px high, small text
.luz-btn--lg
56 px high, base text
.luz-btn--block
Fills the width of its container, and its label wraps: for a card, a form or a drawer, not for a wide column
<svg> inside
An icon at 16 × 16 px, before or after the label, a small gap from it, in the colour of the text
disabled
Muted text, dashed border, no pointer events

The default height is 44 px. The icon is 16 px at every size.

How it works

A native <button> (or <a>) with classes from components/button.css. No JavaScript.

  • Set type="button" unless the button submits a form; the default type is submit.
  • The height is a minimum: the label can grow with the reader's font size.
  • .luz-btn--block sets inline-size: 100%. Put it where the button is the only thing in its row: in a flex row it would take the whole row. Its label wraps, and a word longer than the box breaks, so nothing leaves the button at 320 px or at 200 % text.

Without JavaScript

A button works like any native button: it submits its form or runs nothing. What it does on click is up to your script.

On this page, Try it and the Width choice under the example need JavaScript: without it they are not shown, and the examples and their markup are.

Keyboard and ARIA

Tab reaches the button; Enter or Space presses it. A link styled as a button answers to Enter only, as every link does.

  • No ARIA is needed: the visible label is the name.
  • An icon is aria-hidden="true" and focusable="false": the label names the button. An icon-only button takes its name from a .u-sr-only span, not an aria-label: the span is translated with the page.
  • A disabled button is skipped by Tab and screen readers may not find it: prefer an enabled button that explains what is missing.

Contrast and focus are in the Design tab.

Design

When to use

Use a button to start an action on the current page: save, send, delete, open a dialog.

  • One primary button per view, for the action most people take.
  • Secondary, outline or ghost buttons for the other actions.
  • The full width (.luz-btn--block) when the action is the only thing in a narrow container: a card, a drawer, the submit of a form on a phone.

When not to use

Do not use a button to go to another page: use a link, styled as a button only when it is the main call to action.

  • For several related actions in a row, use an action group.
  • Do not disable a button to hide an action the reader cannot take yet: say why instead.
  • Do not stretch a button across a wide column: a bar 1,000 px wide is not a button. Two actions side by side are two buttons of their own width.

Do and don't

Do One primary button, for the action most people take; the others are quieter.
Don't Two primary buttons: neither one reads as the next step, so the reader has to stop and compare.

Writing the content

Start with a verb and say the result: "Save changes", "Delete draft".

  • Never "OK", "Submit" or "Click here".
  • Sentence case, two or three words, no full stop.
  • Use the same words for the same action everywhere.
  • A full-width bar invites a long label: keep it under about 24 characters, or let it wrap.

Contrast and focus

Filled buttons put white text on a fill; outline and ghost buttons put brand text on the page.

PartColour on backgroundLightDarkHigh contrastNeeds
Primary label
--luz-text-on-fill on --luz-brand-fill
6.0:1
4.9:1
14.1:1
4.5:1
Primary label, hover
--luz-text-on-fill on --luz-brand-fill-hover
8.2:1
7.7:1
17.9:1
4.5:1
Secondary label
--luz-text-primary on --luz-surface-strong
13.5:1
10.9:1
18.0:1
4.5:1
Secondary label, hover
--luz-text-on-fill on --luz-outline
4.6:1
4.8:1
21.0:1
4.5:1
Outline and ghost label
--luz-brand-primary on --luz-surface-page
5.6:1
7.7:1
14.1:1
4.5:1
Outline and ghost label, hover
--luz-brand-primary on --luz-brand-subtle over --luz-surface-page
5.1:1
6.5:1
13.6:1
4.5:1
Danger label
--luz-text-on-fill on --luz-color-danger-fill
5.6:1
6.0:1
9.3:1
4.5:1
Disabled label and border
--luz-text-disabled on --luz-surface-page
3.5:1
4.2:1
5.3:1
3:1
Focus ring
--luz-focus-ring on --luz-surface-page
5.6:1
7.7:1
21.0:1
3:1

Measured from the token values of each mode, rounded down. Needs is the WCAG 2.2 AA minimum: 4.5:1 for text, 3:1 for focus rings, borders and icons. Disabled text has no WCAG minimum; LUZ asks 3:1.

  • Focus: a 2 px --luz-focus-ring outline, 2 px outside the button, on every variant.
  • Not colour alone: a disabled button has a dashed border; the outline variant's border is the brand colour, as its text.
  • Target: 44 px high by default, 36 px for .luz-btn--sm, with or without the full width: use the default size where the target is a thumb (44 px is the enhanced target of WCAG 2.5.5, level AAA).

Tokens it uses

The 26 tokens that components/button.css reads, by family. Each family links to its page, with the values in every mode. In Figma they are variables with the same names: see Tokens in Figma.

Spacing and sizing
--luz-control-lg, --luz-control-md, --luz-control-xl, --luz-space-2xs, --luz-space-lg, --luz-space-sm, --luz-space-xl, --luz-space-xs
Typography
--luz-font-sans, --luz-text-base, --luz-text-md, --luz-text-sm
Radius
--luz-radius-md
Motion
--luz-duration-fast

Keyboard and ARIA are in the Code tab.