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

Content

Labels are short verb phrases — “Delete”, “Cancel”, “Save changes” — not sentences, and carry no trailing punctuation.

Props API

React@ghds/react

PropTypeDefaultDescription
variant'primary' | 'danger' | 'neutral''primary'Visual intent.
asChildbooleanfalseRender as the single child element (Radix Slot) instead of a <button>.
disabledbooleanfalseDisables interaction.
type'button' | 'submit' | 'reset''button'Native button type.
...restButtonHTMLAttributesAll other native <button> attributes pass through.

Web Components@ghds/web-components

PropTypeDefaultDescription
variant'primary' | 'danger' | 'neutral''primary'Reflected attribute.
type'button' | 'submit' | 'reset''button'Drives form association (formAssociated = true).
disabledbooleanfalseReflected attribute.

React Native@ghds/react-native

PropTypeDefaultDescription
labelstringVisible, accessible label — required (no children).
onPress(event: GestureResponderEvent) => voidPress handler.
variant'primary' | 'danger''primary'No neutral variant on React Native.
disabledbooleanfalseDisables interaction.
testIDstringTest handle for queries.
accessibilityHintstringSupplementary 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.

React @ghds/react

Web Components @ghds/web-components

Primary Delete Cancel Disabled