Button
The reference implementation of the GHDS component documentation standard.
This page is the reference implementation of the GHDS component documentation standard — every future component page (Card, Input, and everything in M10/M11) follows the same eight sections in the same order.
Overview
A Button triggers a single, immediate, user-initiated action — submitting a form, confirming a
choice, opening something. Use a link (or asChild with an <a>) instead when the destination is
navigation, not an action.
Anatomy
A real <button> (or a Radix Slot-rendered element via asChild) carries all native keyboard
and click semantics. A hand-drawn SketchSurface outline sits behind the label, purely decorative
(aria-hidden="true", focusable="false", with an SVG <title> for tooling) — it never carries
meaning, so removing it never changes what the button does or how it’s announced.
Variants & States
Exactly three variants exist: primary, danger, neutral — there is no secondary, ghost, or
link variant.
| State | Status | Notes |
|---|---|---|
| Default | Implemented | — |
| Hover | Implemented | Fill/text color shifts per variant. |
| Active / pressed | Implemented | — |
| Focus | Implemented | A manual focus ring (comp.button.focus.ring), not native :focus-visible. |
| Disabled | Implemented | — |
| Loading | Not implemented | — |
| Error | Not implemented | — |
Usage
Do
- Use danger only for destructive or irreversible actions.
- Use asChild to keep link semantics when a button needs to look and behave like a link.
- Keep one primary button per view — it should be obvious what the main action is.
Don't
- Don't rely on color alone to convey a danger action — the label should say what happens.
- Don't disable a button without explaining why somewhere else on screen.
- Don't nest an <a> inside a native <button> — use asChild instead.
Accessibility
- Native
<button>keyboard behavior (Space/Enter activates, native tab order) comes for free — no customonKeyDownortabIndexexists in the implementation. - Disabling is not uniform across the two render paths: the native
<button disabled>path removes the element from the tab order entirely (the browser’s native behavior), while theasChildpath only setsaria-disabled— which does not remove the element from the tab order. If you disable anasChildbutton, you’re responsible for also preventing activation. - See the Accessibility Guide for the general keyboard and ARIA conventions shared across all components.
Content
Labels are short verb phrases — “Delete”, “Cancel”, “Save changes” — not sentences, and carry no trailing punctuation.
Props API
React@ghds/react
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'primary' | 'danger' | 'neutral' | 'primary' | Visual intent. |
asChild | boolean | false | Render as the single child element (Radix Slot) instead of a <button>. |
disabled | boolean | false | Disables interaction. |
type | 'button' | 'submit' | 'reset' | 'button' | Native button type. |
...rest | ButtonHTMLAttributes | — | All other native <button> attributes pass through. |
Web Components@ghds/web-components
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'primary' | 'danger' | 'neutral' | 'primary' | Reflected attribute. |
type | 'button' | 'submit' | 'reset' | 'button' | Drives form association (formAssociated = true). |
disabled | boolean | false | Reflected attribute. |
React Native@ghds/react-native
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Visible, accessible label — required (no children). |
onPress | (event: GestureResponderEvent) => void | — | Press handler. |
variant | 'primary' | 'danger' | 'primary' | No neutral variant on React Native. |
disabled | boolean | false | Disables interaction. |
testID | string | — | Test handle for queries. |
accessibilityHint | string | — | Supplementary a11y hint describing the action's outcome. |
Code examples
// React — @ghds/react
import { Button } from '@ghds/react/button';
<Button variant="primary">Primary</Button>;
<Button variant="danger">Delete</Button>;
<Button variant="neutral">Cancel</Button>;
<!-- Web Components — @ghds/web-components -->
<script type="module">
import '@ghds/web-components/button';
</script>
<gh-button variant="primary">Primary</gh-button>
<gh-button variant="danger">Delete</gh-button>
<gh-button variant="neutral">Cancel</gh-button>
// React Native — @ghds/react-native (no 'neutral' variant; label string + onPress, no children)
import { Button } from '@ghds/react-native/button';
<Button label="Delete" variant="danger" onPress={() => {}} />;
Live Demo
React and Web Components render live below, driven by the same tokens — toggle dark mode in the
header to see both respond identically. React Native can’t run in a browser without additional
react-native-web wiring (not set up in this site), so its usage is shown as a code sample instead.