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.
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:
<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:
<button class="luz-btn luz-btn--primary" type="button">Save changes</button>
A link that goes somewhere, with the look of a button:
<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:
<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:
<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 attribute | Effect |
|---|---|
.luz-btn | Brand fill: the main action of the view |
.luz-btn | Strong neutral surface: a second action |
.luz-btn | Brand text and border, no fill |
.luz-btn | Brand text only: a low-priority action |
.luz-btn | Danger fill: an action that deletes or cannot be undone |
.luz-btn | 36 px high, small text |
.luz-btn | 56 px high, base text |
.luz-btn | 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 issubmit. - The height is a minimum: the label can grow with the reader's font size.
.luz-btn--blocksetsinline-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"andfocusable="false": the label names the button. An icon-only button takes its name from a.u-sr-onlyspan, not anaria-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
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.
| Part | Colour on background | Light | Dark | High contrast | Needs |
|---|---|---|---|---|---|
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-ringoutline, 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.
- Color
--luz-brand-fill,--luz-brand-fill-active,--luz-brand-fill-hover,--luz-brand-primary,--luz-brand-subtle,--luz-color-danger-fill,--luz-focus-ring,--luz-outline,--luz-surface-strong,--luz-text-disabled,--luz-text-on-fill,--luz-text-primary- 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.