Button
No border, no ornament — hierarchy is ink density, not an outline. Five variants, three sizes, one geometry.
Variants
default is quiet — no fill at rest, a flat tint of the stock on hover, the
same gesture as a CardRow. primary is the accent fill — one committing
action per view, at most. dark is reserved for the single most committing
action in a screen, typically a final Save; it should be rare. ghost is
the same quiet block one step further back — text-secondary instead of
text-primary — for dismissals and tertiary actions. danger is destructive
and irreversible only.
Markup
<button>Cancel</button>
<button data-variant="primary">Save</button>
<button data-variant="dark">Finish setup</button>
<button data-variant="ghost">Dismiss</button>
<button data-variant="danger">Delete</button>
Sizes
Sizes move one rung down the space scale, and at the two ends of the ramp one step of font size with it — weight, radius, and letter-spacing never move. Medium and large pad themselves with the same rung and part on type size instead; that is what holds their order at every width, since the space scale is fluid and the size on the wider rung would otherwise overtake the one on the narrower.
Being fluid, a medium button runs 35–38px tall depending on the width of the window. IconButton builds its square from the same two rungs, so the two are exactly the same height at every width and in every size — a button and an icon button in a toolbar line up without either being told the other’s measurements.
Markup
<button data-variant="primary" data-size="sm">Small</button>
<button data-variant="primary">Medium</button>
<button data-variant="primary" data-size="lg">Large</button>
States
Disabled buttons drop to 50% opacity and keep their variant colors, so a disabled primary still reads as the primary.
Markup
<button data-variant="primary" disabled="">Disabled</button>
<button aria-disabled="true">Not yet available</button>
disabled vs. aria-disabled
A disabled button is removed from the tab order, so a reader
navigating by keyboard never learns it is there. When a button is
temporarily unavailable and the reason matters, use
aria-disabled="true" instead — it stays focusable and can
carry an explanation.
With an icon
Pass an icon as the first child. The gap is --gl-space-2xs, set by the
button itself.
Markup
<button data-variant="primary">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14" /></svg>
New
</button>
Groups
[role="group"] is already the correct ARIA role for a toolbar of related
buttons, so it doubles as the styling hook — no wrapper class.
Markup
<div role="group" aria-label="View">
<button data-size="sm">List</button>
<button data-size="sm">Board</button>
<button data-size="sm">Calendar</button>
</div>
Full width
Markup
<button data-variant="primary" data-block>Take a photo</button>
Links that act like buttons
An <a> with .btn is fine when the thing it does is navigate. A <button>
is required when it changes something. .btn exists only because a bare a
selector would catch every ordinary prose link on the site — it is the one
class Button needs, and only anchors reach for it.
Markup
<a class="btn" data-variant="primary" href="/gloss/components/icon-button/">Next: IconButton</a>
<a class="btn" data-variant="ghost" href="/gloss/foundations/color/">Back to color</a>
API
| Element / attribute | Values | Notes |
|---|---|---|
button | Required base. A bare <button> is styled directly — no class. Use <a class="btn"> instead when the control navigates. | |
data-variant | primary | dark | ghost | danger | Omit for the default, quiet look — no fill at rest. |
data-size | sm | lg | Omit for the default (medium) size. Padding, plus one step of font size at each end. |
data-block | boolean | Full width of its container. |
disabled | boolean | Removes the button from the tab order. |
aria-disabled | "true" | Looks disabled, stays focusable. |
[role="group"] | Wrap adjacent buttons in a group to join their radii; a hairline divider separates them, since the buttons themselves carry no border. |