Button

Six intents × three sizes (plus the icon square) × disabled / focus / hover states. Always semantic: the intent communicates what the button does (default = the primary action, destructive = irreversible, ghost = chrome).
On the web every intent is a pill: the call to action is the green action pill, secondary a violet outline, outline a hairline, ghost and link bare. A primary button rises 1 px under the pointer over 150 ms and settles back when it leaves, the outline looks take a hover wash at once, and a link dims; a disabled or loading button stays put, and Reduce Motion makes the lift instant. A disabled web button goes transparent with a hairline and a muted label rather than fading. On iOS and Android the buttons keep their platform shapes in the theme's colours.
Pass ref to access the interactive Pressable, including link buttons. Use useRef<ComponentRef<typeof Button>>(null) from React, or useRef<View>(null) with React Native's View type. Object and callback refs are supported and detach on unmount. Calling ref.current?.focus() or .blur() delegates to the host without activating the control. Browser focus is supported; native focus depends on the platform and React Native version, and is separate from accessibility focus.
iOS
Android
Web
tsx
<Button>Save changes</Button>
The variant only changes how a button looks; every one of them fires onPress the same way.
A button's whole job is to fire onPress. Wire it to your own handler and every press runs it.

Props

Prop
Description
children
ReactNode
—
iconLeft
ReactNode
An icon element rendered before the label (e.g. a lucide or Canvas `Icon`). It renders directly (not wrapped in the label `Text`, which cannot host an SVG), spaced from the label by the skin's gap. Pass `iconLeft` alone with the `icon` size prop for an icon-only square button. The caller sizes and colors the icon (use the intent's foreground for non-ghost intents).
iconRight
ReactNode
An icon element rendered after the label (e.g. a trailing chevron/arrow).
accessibilityLabel
string
Accessibility label, required for an icon-only button (no text to read).
onPress
(event: GestureResponderEvent) => void
—
href
string
Renders the button as a REAL link on the web: react-native-web gives the root Pressable an `<a href>`, so middle-click, cmd-click, open-in-new-tab, and crawlers all see the destination, and assistive tech hears a link. Native platforms have no anchors and ignore it, so pair `href` with an `onPress` that runs the same navigation through the host's router. While `disabled` or `loading` the anchor is suppressed, exactly like `onPress`.
hrefAttrs
{ target?: "_blank" | "_self"; rel?: string; download?: boolean | string }
Anchor attributes forwarded with `href` on the web (react-native-web's `hrefAttrs`): `target`, `rel`, `download`. Ignored without `href`, and everywhere natively.
onHoverIn
(event: MouseEvent) => void
Called when a pointer starts hovering the button. Fires on pointer devices only (web via react-native-web, pointer-equipped iPads/desktops natively); touch never triggers it. For hover-driven disclosure such as Tooltip.
onHoverOut
(event: MouseEvent) => void
Called when the pointer stops hovering the button (see `onHoverIn`).
onFocus
(event: NativeSyntheticEvent<TargetedEvent>) => void
Called when the button gains focus (keyboard tab, or click on web).
onBlur
(event: NativeSyntheticEvent<TargetedEvent>) => void
Called when the button loses focus.
primary
boolean
Intent (pick one; default is the primary action).
secondary
boolean
Intent (pick one; default is the primary action).
destructive
boolean
Intent (pick one; default is the primary action).
outline
boolean
Intent (pick one; default is the primary action).
ghost
boolean
Intent (pick one; default is the primary action).
link
boolean
Intent (pick one; default is the primary action).
small
boolean
Size (pick one).
large
boolean
Size (pick one).
icon
boolean
Size (pick one).
block
boolean
Layout and state. A Button is HUG (src/style/sizing.ts): it keeps its content width inside a stretching Column and sits content-sized in a Row; `block` makes it FILL (full width in a Column, an equal share in a Row), and a measure step (`sm`, `lg`, …, from MeasureProps) makes it FILL up to that width of the scale.
loading
boolean
Layout and state. A Button is HUG (src/style/sizing.ts): it keeps its content width inside a stretching Column and sits content-sized in a Row; `block` makes it FILL (full width in a Column, an equal share in a Row), and a measure step (`sm`, `lg`, …, from MeasureProps) makes it FILL up to that width of the scale.
disabled
boolean
Layout and state. A Button is HUG (src/style/sizing.ts): it keeps its content width inside a stretching Column and sits content-sized in a Row; `block` makes it FILL (full width in a Column, an equal share in a Row), and a measure step (`sm`, `lg`, …, from MeasureProps) makes it FILL up to that width of the scale.
raised
boolean
Lifts a primary button off the page with a soft glow in its own colour: the page's main call to action (Dark Factory's raised button). Other intents, and a disabled button, ignore it.
expanded
boolean
Marks the Button as a disclosure/menu trigger and announces its open state to assistive tech (aria-expanded, which react-native-web forwards and RN maps back to the native expanded state). Pass the live open boolean; omit for a plain button. So a Button that toggles a Dropdown/Popover stays accessible.
haspopup
"menu" | "dialog" | "listbox" | "grid" | "tree" | true
Announces that the button opens a popup of this kind (aria-haspopup), for a menu / dialog / listbox trigger. Pair it with `expanded` so assistive tech reads both the popup relationship and its open state. Web-only; a no-op natively.
testID
string
E2E hook forwarded to the root element.
style
LayoutStyle
Composition within a parent only, never a restyle hook and never a width: `block`, the measure steps, and the parent layout container own the width.
xxxs
boolean
Measure (pick one; omit to take the parent's bounds). Caps the component at a step of the width scale, centered in its column; `start` pins it to the leading edge.
xxs
boolean
Measure (pick one; omit to take the parent's bounds). Caps the component at a step of the width scale, centered in its column; `start` pins it to the leading edge.
xs
boolean
Measure (pick one; omit to take the parent's bounds). Caps the component at a step of the width scale, centered in its column; `start` pins it to the leading edge.
sm
boolean
Measure (pick one; omit to take the parent's bounds). Caps the component at a step of the width scale, centered in its column; `start` pins it to the leading edge.
md
boolean
Measure (pick one; omit to take the parent's bounds). Caps the component at a step of the width scale, centered in its column; `start` pins it to the leading edge.
lg
boolean
Measure (pick one; omit to take the parent's bounds). Caps the component at a step of the width scale, centered in its column; `start` pins it to the leading edge.
xl
boolean
Measure (pick one; omit to take the parent's bounds). Caps the component at a step of the width scale, centered in its column; `start` pins it to the leading edge.
xxl
boolean
Measure (pick one; omit to take the parent's bounds). Caps the component at a step of the width scale, centered in its column; `start` pins it to the leading edge.
xxxl
boolean
Measure (pick one; omit to take the parent's bounds). Caps the component at a step of the width scale, centered in its column; `start` pins it to the leading edge.
wide
boolean
Measure (pick one; omit to take the parent's bounds). Caps the component at a step of the width scale, centered in its column; `start` pins it to the leading edge.
wider
boolean
Measure (pick one; omit to take the parent's bounds). Caps the component at a step of the width scale, centered in its column; `start` pins it to the leading edge.
widest
boolean
Measure (pick one; omit to take the parent's bounds). Caps the component at a step of the width scale, centered in its column; `start` pins it to the leading edge.
page
boolean
Measure (pick one; omit to take the parent's bounds). Caps the component at a step of the width scale, centered in its column; `start` pins it to the leading edge.
start
boolean
Pin a capped component to the leading edge instead of centering it in its column.
ref
React.Ref<View>
Ref to the interactive Pressable, including link buttons. Typed as a React Native View. On web, React Native Web exposes its DOM host; focus() and blur() move browser focus. Native host behavior depends on the platform and React Native version. Calling focus() does not activate the control or call accessibility focus APIs.

Do & Don’t

Default (primary)

Don’t
Multiple primaries compete; nothing stands out.
Do
One clear primary action; everything else is supporting.

Outline

Don’t
All-outline leaves no signal which action is primary.
Do
Promote the main action to default; keep the rest outline.

Secondary

Don’t
A secondary button as the main call to action under-sells it.
Do
Default for the primary action; secondary for the next one down.

Ghost

Don’t
A ghost button is too quiet to carry the primary action.
Do
Use ghost for tertiary and toolbar actions; keep the CTA filled.

Destructive

Don’t
Red on a safe action cries wolf; users learn to ignore it.
Do
Reserve the destructive variant for irreversible actions like delete.

Link

Don’t
A link-styled submit doesn't look pressable and gets lost.
Do
Link variant for inline navigation; a filled button for the submit.

Real links (href)

A button that NAVIGATES should be a real link, not a press handler that sets location. Pass href and the web render becomes a genuine browser link: middle-click, cmd-click, and open-in-new-tab all work, crawlers see the destination, and assistive tech hears a link. Native platforms have no anchors, so pair href with an onPress that runs the same navigation through your router; while disabled or loading the anchor is suppressed exactly like onPress. hrefAttrs carries the anchor attributes react-native-web forwards (target, rel, download).
tsx
<Row alignCenter cozy>
<Button link href="https://canvas.nannier.com">Read the docs</Button>
<Button outline href="https://www.npmjs.com/package/@nannier/canvas" hrefAttrs={{ target: "_blank", rel: "noreferrer" }}>
npm package
</Button>
</Row>

Touch area

On iOS and Android a Button smaller than the platform's minimum touch target (44pt, 48dp) keeps its size and extends only its touch area to the minimum, from its first frame, so nothing moves. React Native bounds that area by two rules of its own. It never reaches past a native container that does not contain it: a container of yours that clips (overflow hidden, a scroll view's edge) stops it at that edge. A container that paints, carries a testID or handles pointer events admits it, because such a container records the touch area at its layout and the Button has it from its first one. Every kit control does the same for itself; a control the kit lacks belongs in the kit, never in a Pressable of your own (see DESIGN.md, Shapes). And where two touch areas overlap, the later sibling takes the tap: on Android two small icon Buttons 8 apart in a Row both reach across the gap between them, so a tap anywhere in it presses the second. Leave at least twice the extra touch area between small Buttons you place side by side (16 between two small icon Buttons on Android, 8 on iOS).