Skip to content
Components

Button

Every clickable commitment renders through here. It picks the right element for the job and makes loading safe.

Variants

The variant carries colour ONLY. Geometry lives on the base, so a variant can never drift in size — link is the one exception, and the exception is the point.
Code
<GdButton variant="primary">Primary action</GdButton>
<GdButton variant="solid">Open the map</GdButton>
<GdButton>Secondary</GdButton>
<GdButton variant="ghost">Ghost</GdButton>
<GdButton variant="link">Use a different email</GdButton>
<GdButton variant="danger">Destructive</GdButton>

Solid

The filled neutral-ink CTA, for a view whose accent is already spoken for. The canonical pair is a nav: link on "Sign in", solid on "Open the map". An accent primary in that slot fights whatever else on the page is already accent-coloured.

It paints itself with --gd-surface-solid and --gd-ink-on-solid — the one neutral pair that genuinely flips with the theme, near-black on a light page and near-white on a dark one. Not --gd-surface-inverse, which is deliberately dark in both themes so that a toast, a code block and the footer band stay dark at night. Borrowing inverse here is what shipped a 1.3:1 pill on a charcoal bar — a control with no visible boundary, which is a WCAG 1.4.11 failure and, more plainly, a call to action nobody could see.

There is no --gd-surface-solid-hover to hunt for: solid mixes the ink that already sits ON the fill back over it, which moves the pill toward its own label — lighter on a light page, darker on a dark one. One color-mix rule lifts the hover in each theme without a second token.
Code
<GdButton variant="link">Sign in</GdButton>
<GdButton variant="solid">Open the map</GdButton>

Link

A real button that reads as a link, for actions that live in a sentence. An <a href="#"> that performs an action misreports itself to everyone navigating by links; this variant exists so authors stop reaching for that anchor.

A six-digit code is on its way. , or .

The only variant that changes GEOMETRY rather than colour — the box comes off, because a link wearing a padding box is a button in a costume. The size ladder still sets the type size, so it can be told to match the sentence it sits in.
Code
<p>A six-digit code is on its way.
  <GdButton variant="link" size="sm">Use a different email</GdButton>
</p>

With an icon

The side carrying the glyph loses one rung of padding. A glyph is lighter than a letterform and sits in its own box, so at equal padding it reads as pushed inward — and the larger the button, the more it shows.
Code
<GdButton variant="primary">
  <template #leading><GdIcon name="check" :size="15" :stroke="2.4" /></template>
  Save the report
</GdButton>

<GdButton variant="ghost">
  Continue
  <template #trailing><GdIcon name="chevron-right" :size="15" :stroke="2.4" /></template>
</GdButton>
Every size compensates12→10, 18→14, 24→18 — one rung down at each size, and every value is a real rung.
Code
<GdButton size="sm"> … </GdButton>
<GdButton size="md"> … </GdButton>
<GdButton size="lg"> … </GdButton>

Sizes

Icon buttons

34px is under the 44px touch minimum, so an icon button gets the target back with a pseudo-element rather than by growing. The visual size is a design decision and the tap size is an accessibility one — they are allowed to differ.

States

Loading keeps the variant's own colour — a button mid-action has not become a different button, and repainting it grey reads as failure.
Code
<GdButton variant="primary" :loading="busy" loading-label="Working…" @click="save">
  Save
</GdButton>

Block

API

Props

PropTypeDefault
variant"primary" | "solid" | "secondary" | "ghost" | "link" | "danger" | "inverse""secondary"

Colour only — geometry lives on the base, so a variant can never change a button's size. `link` is the one exception and the exception is the point.

size"xs" | "sm" | "md" | "lg""md"

`xs` exists for the 24px icon circle, where the container's height is the whole budget.

blockbooleanfalse

Full width. A form column's default.

disabledbooleanfalse

Inert and unfocusable. Prefer leaving the control live and explaining the failure — see the callout below.

loadingbooleanfalse

Disables activation and shows a spinner beside the label rather than replacing it, so the button does not resize mid-action.

loadingLabelstring—

What to say while loading. Without it the label stays as-is.

pressedboolean—

A LATCH — on and staying on. Sets `aria-pressed` from the same value that draws the held-down look. Left undefined the button is not a toggle at all, which is why it has no `false` default.

dashedbooleanfalse

A dashed edge: an affordance for adding something that is not there yet.

donebooleanfalse

It worked — a tick and `doneLabel`. The CALLER clears it; the button will not reset itself.

doneLabelstring—

What to say in the done state.

iconbooleanfalse

A square button holding one glyph. Needs `label`, and drops the text-label wrapper entirely.

labelstring—

The accessible name. Required when the button has no text — an icon-only control without it is announced as just "button".

tostring—

Internal route. Renders a NuxtLink.

hrefstring—

External URL. Renders an `<a>`.

type"button" | "submit" | "reset""button"

Only meaningful on a real `<button>` — ignored when `to` or `href` makes it a link.

Slots

default

The label.

leading

A glyph before the label. Hidden while `loading` swaps the label.

trailing

A glyph after the label.

Usage Guidelines

  • Use primary for the single most important next step in a VIEW — not per card, not per section. Two primaries on a screen means neither is.
  • Use solid when the accent is already spoken for and the view still needs one committed next step. The canonical pair is a nav: link on "Sign in", solid on "Open the map".
  • Use secondary for everything else that is a real action. It is the default because most buttons are not the most important one.
  • Use ghost inside dense chrome — a toolbar, a panel header — where a bordered button per control would read as a grid of boxes.
  • Use danger only with a second confirmation. Never as the sole guard on a destructive action.
  • Reach for to or href, not a click handler, whenever the result is a new location. That is what makes middle-click and open-in-new-tab work.
  • Do not use disabled to mean "not yet". Leave the control live and say what is missing — a disabled button explains nothing and cannot be focused to ask.

Behavior & Anatomy

Which element it renders

The part hand-rolled buttons keep getting wrong.

<GdButton to="/library"> → NuxtLink <GdButton href="https://…"> → <a> <GdButton> → <button type="button">

A div with a click handler is not on the table: it answers Enter, silently ignores Space — which every native button activates on — and is unreachable by keyboard without a tabindex nobody remembers to add. Real navigation also means middle-click and open-in-new-tab work, which a click handler cannot give you.

Navigate

Esc