Button
Every clickable commitment renders through here. It picks the right element for the job and makes loading safe.
Variants
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.
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 .
Code
<p>A six-digit code is on its way.
<GdButton variant="link" size="sm">Use a different email</GdButton>
</p>With an icon
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>Code
<GdButton size="sm"> … </GdButton>
<GdButton size="md"> … </GdButton>
<GdButton size="lg"> … </GdButton>Sizes
Icon buttons
States
Code
<GdButton variant="primary" :loading="busy" loading-label="Working…" @click="save">
Save
</GdButton>Block
API
Props
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.
blockbooleanfalseFull width. A form column's default.
disabledbooleanfalseInert and unfocusable. Prefer leaving the control live and explaining the failure — see the callout below.
loadingbooleanfalseDisables 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.
dashedbooleanfalseA dashed edge: an affordance for adding something that is not there yet.
donebooleanfalseIt worked — a tick and `doneLabel`. The CALLER clears it; the button will not reset itself.
doneLabelstring—What to say in the done state.
iconbooleanfalseA 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
defaultThe label.
leadingA glyph before the label. Hidden while `loading` swaps the label.
trailingA glyph after the label.
Usage Guidelines
- Use
primaryfor the single most important next step in a VIEW — not per card, not per section. Two primaries on a screen means neither is. - Use
solidwhen the accent is already spoken for and the view still needs one committed next step. The canonical pair is a nav:linkon "Sign in",solidon "Open the map". - Use
secondaryfor everything else that is a real action. It is the default because most buttons are not the most important one. - Use
ghostinside dense chrome — a toolbar, a panel header — where a bordered button per control would read as a grid of boxes. - Use
dangeronly with a second confirmation. Never as the sole guard on a destructive action. - Reach for
toorhref, 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
disabledto 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.