Skip to content
Protocore Design Systemv1.6.9

/// Inputs

Button

The system action control — mono uppercase label, sharp corners, and a primary fill that inverts on hover.

import { Button } from "@protocore/pds";
View as Markdown

Basics

A Button triggers an action in place. It renders a native <button type="button"> — attach your own onClick, or set type="submit" inside a form.

Variants

Four visual weights. primary inverts to an outline on hover; secondary and ghost are progressively quieter; danger is a destructive outline that fills on hover. Use exactly one primary per view.

Sizes

Control heights track the shared scale: sm 32 · md 36 · lg 40 · xl 48. full stretches the button to fill its container for form and mobile CTAs.

With icons

startIcon and endIcon take any node — size lucide glyphs to 16px so they align with the mono label. Icons are decorative (aria-hidden); the label carries the meaning.

Loading and disabled

loading swaps the leading icon for a spinner, sets aria-busy, and disables the control while a background action runs. A plain disabled button is removed from the tab order.

Composed: a confirmation flow

Destructive actions earn a second step. Here a danger button reveals an inline Cancel / Confirm pair (a ButtonGroup), and the confirm button shows its loading state while the work runs.

When to use

Reach for Button when activating it *does* something in place — submit a form, deploy a service, open a dialog, run a job.

  • Navigating to another route or URL? Use Link (even when it looks like a button) so it renders a real <a> with correct semantics, or pass asChild and wrap an anchor.
  • A compact, icon-only action in a toolbar? Use IconButton, which requires an aria-label.
  • Holding a *selected* state that filters or toggles a view? Use Chip — it is a pressed toggle, not a one-shot action.

Do & don't

Do

  • Lead each view with a single primary Button and make everything else secondary or ghost.
  • Reserve variant="danger" for destructive actions, and pair it with a confirmation step.
  • Write labels as short verb phrases — "Deploy service", "Export ledger".
  • Use asChild to render a real <a> whenever the control navigates.

Don't

  • Don't stack multiple primary buttons competing for the same attention.
  • Don't use a Button for pure navigation — an anchor styled as a button breaks middle-click and right-click.
  • Don't disable the submit button as your only validation feedback; say what's missing.
  • Don't pad labels with prose — one or two words, no trailing punctuation.

Accessibility

KeysAction
Tab / Shift+TabMove focus onto / off the button.
Space or EnterActivate the button.
  • Renders a native `<button type="button">`; set `type="submit"` when it submits a form.
  • `loading` sets `aria-busy` and disables the control; the spinner and icon slots are `aria-hidden`.
  • Disabled buttons leave the tab order — don't rely on disabling alone to explain why an action is unavailable.
  • Icon-only actions must carry a visible label or an `aria-label`; use IconButton for that case.

Mobile (React Native)

Preview. @protocore/pds-mobile ships the React Native sibling of Button. It mirrors the web API where React Native allows; the package is a preview with no device-level QA yet, so pin it and expect small changes.

Import it from the mobile package (not @protocore/pds), inside a <PdsProvider> — there is no stylesheet, so style (a ViewStyle) replaces className and every value comes from the theme:

import { Button } from "@protocore/pds-mobile";

Parity with web. It renders a Pressable, not an HTML <button>, and has no asChild — React Native has no polymorphic Slot.

  • There is no :hover on touch: primary inverts (and danger fills) on press via Pressable's pressed state, not hover.
  • Use onPress instead of onClick; for navigation, wrap your router's pressable or pass an onPress handler.
  • a11y is built in: accessibilityRole="button" plus accessibilityState { disabled, busy } (set by loading).
<Button variant="primary" size="md" onPress={() => deploy()}>
  Deploy service
</Button>

Button props

PropTypeDefaultDescription
asChildbooleanfalseRender as the single child element (Radix `Slot`) instead of a `<button>` — e.g. wrap a link. Icon slots are not injected in this mode.
classNamestring
endIconReactNodeTrailing icon slot (any `ReactNode`). Ignored when `asChild`.
fullbooleanfalseStretch to fill the width of the container (block button).
loadingbooleanfalseShow an inline spinner, set `aria-busy`, and disable the control while a background action runs.
sizeenummdControl height + horizontal padding: sm 32 · md 36 · lg 40 · xl 48.
startIconReactNodeLeading icon slot (any `ReactNode`). Suppressed while `loading` and ignored when `asChild`.
styleCSSProperties
variantenumprimaryVisual weight. `primary` inverts on hover; `secondary`/`ghost` are quieter; `danger` is a destructive outline that fills on hover.

Related

  • IconButtonA square, icon-only action control — Button's affordances with a required aria-label.
  • ButtonGroupA horizontal cluster of Buttons or IconButtons — seamed into one control by default, or spaced by a gap.
  • ChipAn interactive filter toggle — mono uppercase ghost outline that inverts to the primary fill when selected.
  • LinkInline text link — ink with a control-hairline underline that inks in on hover.