Button

Atom
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).

Playground

Every control sets a real prop of ButtonProps. The JSX below updates as you go.
Props
Intent
pick one
Size
pick one
children
string
iconLeft
icon
iconRight
icon
disabled
boolean
loading
boolean
raised
boolean
block
boolean
attention
boolean
count
string
JSX · ButtonProps
<Button>Save changes</Button>

Variants

20 examples from src/atoms/button/button.md, each rendered live from its own JSX.
Attention required
Use attention for a required setup action. Soft red glow swells outward and returns in a heartbeat rhythm: a strong beat, a softer beat, then a pause every 1.8 seconds. Its inner edge, label and button dimensions stay still. Reduce Motion keeps a steady halo; disabled and loading buttons suppress it. Remove the button or the prop when the required setup is complete.
<Button outline small attention>Configure AI</Button>
CORE-1790
Inline identity and reference actions
<Row comfortable alignCenter>
<Button identityAction ghost steadyPress iconLeft={<Avatar annotation initials="S" hue={20} />}>Scout v2.3</Button>
<Button referenceLink inlineLink link steadyPress iconRight={<Icon externalLink size={10} heavyStroke muted />}>CORE-1790</Button>
<Button inlineIcon inlineLink link steadyPress iconLeft={<Icon externalLink size={13} heavyStroke primary />} accessibilityLabel="Open repository" />
</Row>
Theme choices
<Row snug>
<Button themeChoice ghost steadyPress selected iconLeft={<Swatch tiny ring circle decorative color="#f1a6b0" />}>blush</Button>
<Button themeChoice ghost steadyPress iconLeft={<Swatch tiny ring circle decorative color="#80e2b9" />}>mint</Button>
</Row>
Scope and filter triggers
<Row comfortable alignCenter>
<Button scopeTrigger ghost steadyPress iconRight={<Icon chevronDown size={14} heavyStroke />}>All my projects</Button>
<Button filterTrigger ghost steadyPress iconRight={<Icon chevronDown size={13} heavyStroke muted />}>Filter by</Button>
</Row>
Utility icon
<Button icon compactIcon ghost steadyPress accessibilityLabel="Open menu" iconLeft={<Icon menu size={15} boldStroke muted />} />
Notification action
<Button icon largeIcon circle ghost steadyPress count={3} accessibilityLabel="Approvals" iconLeft={<Icon bell size={17} mediumStroke muted />} />
Outline
<Button outline>Save changes</Button>
Secondary
<Button secondary>Save changes</Button>
Ghost
<Button ghost>Save changes</Button>
Destructive
<Button destructive>Save changes</Button>
Save changes
Link
<Button inlineLink link small steadyPress>Save changes</Button>
Small
<Button small>Save changes</Button>
Large
<Button large>Save changes</Button>
Icon only
<Button icon accessibilityLabel="Add item" iconLeft={<Icon plus size={16} />} />
Disabled
A disabled button ignores presses: onPress never runs.
<Button disabled>Save changes</Button>
Loading
<Button loading>Saving</Button>
Raised
raised rests the page's main call to action on a soft glow in its own colour. Only a primary button takes it, and never while disabled.
<Button raised>Mint agent</Button>
Block
<Button block>Create account</Button>
Measure
Use shrink for a content-sized action in a narrow row. The button keeps its normal width when space permits and wraps its full label when constrained.
<Container>
<Column>
<Button xs start>xs step, pinned to the start (320)</Button>
<Button md>md step, centered (448)</Button>
<Button block>Block: fills the parent</Button>
</Column>
</Container>
With icon
<Button iconLeft={<Icon plus size={16} />}>Save changes</Button>

Sizes

small
default
large

States

Hover, focus and press are pinned so they can be compared side by side; the rest are real props.
default
hover
focus
pressed
disabled
loading

Anatomy

1
Container
2
Label
3
Leading icon
4
Trailing icon
5
Count badge
Dimmed parts appear only in some states or variants.

Props

62 props on ButtonProps, grouped as they are declared in source. Required props are marked *.
Prop
Type
Default
Description
Props
12 props
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 the icon; a Canvas Icon with no tone of its own takes the button's label ink (the primary's contrast ink on a primary button, the muted ink on a quiet one, dimmed with the label while disabled), and an explicit tone keeps it.
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.
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.
Intent
pick one
26 props
primary
boolean
false
secondary
boolean
false
inverse
boolean
false
White outlined action over a dark or branded surface.
quiet
boolean
false
Muted outlined action for a low-emphasis operation.
softHover
boolean
false
The reference link hover dims to 85% while keeping its normal ink.
scopeTrigger
boolean
false
A bare project-scope menu trigger: its label set in 600, a hover wash, the chevron through iconRight.
identityAction
boolean
false
An identity action: a 28-tall hairline pill around a leading avatar and its name.
pageLink
boolean
false
A page header's link: a 34-tall hairline box around a muted label (a source path, the page's section).
jumpLink
boolean
false
A page's jump to one of its sections: a 28-tall hairline pill around a muted 12px label.
mono
boolean
false
A pageLink whose label is a path or code: the 400 12px mono face.
referenceLink
boolean
false
A reference-key link: an underlined monospace key in the link ink.
inlineIcon
boolean
false
An unboxed 22px icon action.
themeChoice
boolean
false
A theme choice pill whose selected state is the accent edge and a soft accent halo.
selected
boolean
false
filterTrigger
boolean
false
A bare filter-menu trigger: a muted label, a hover wash, the chevron through iconRight.
inlineLink
boolean
false
Inline text action: an underlined link label with no box, read as a link, washed on hover.
MeasureProps · width steps xxxs 192 · xxs 256 · xs 320 · sm 384 · md 448 · lg 512 · xl 576 · xxl 672 · xxxl 768 · wide 896 · wider 1024 · widest 1152 · page 1280, plus start to pin to the leading edge.

Accessibility and keyboard

Keyboard
Enter
Activates
Space
Activates
Tab
Moves focus on
Semantics
Roles
button
link
States
busy
disabled
selected
ARIA on web
aria-busy
aria-disabled
aria-expanded
aria-haspopup
aria-hidden
Below the platform minimum, the touch area grows to 44 pt on iOS and 48 dp on Android without moving the layout.
From the docs
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 a button is 32, 40 or 48 tall (small, default, large) with the 10 px control corner, a 500-weight label at 13, 14 or 16 px, and an 8 px gap beside its icons. The call to action fills with the accent, destructive with the status red, both under a lit top edge; secondary is the quiet well with a boundary, outline the boundary alone, ghost and link bare (the link underlined). A filled button brightens and rises 1 px under the pointer over 150 ms and settles back when it leaves; the other looks
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.
Utility icon actions use icon with compactIcon (28), mediumIcon (32), or largeIcon (34). wideIcon gives the compact box two extra pixels of horizontal room; circle rounds it fully. Pair with ghost for a plain surface, outline for a boundary, or ghost inset for a quiet fill. steadyPress keeps the resting look while pressed, and count carries a notification badge over the upper trailing corner. The button still owns its accessible name, hover wash, and native touch-target extension.
scopeTrigger ghost steadyPress is a bare 32 px menu trigger with a 600-weight 14px label; filterTrigger ghost steadyPress the same with a muted 13px label. Both take the well's wash on hover. Supply the trailing chevron through iconRight, and announce the menu with expanded and haspopup.
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-nativ

Platform skins

The playground's props on each platform: same tree, native shapes, type and touch sizes.
CORE-1790
Web
control 10 · field 10 · card 16 · Instrument Sans
CORE-1790
iOS
capsule controls · field 8 · card 12 · SF Pro · 44 pt
CORE-1790
Android
Material 3 stadium · field 4 · card 12 · Roboto · 48 dp

Usage

A button's whole job is to fire onPress. Wire it to your own handler and every press runs it.
<Button>Save changes</Button>
On the web a button is 32, 40 or 48 tall (small, default, large) with the 10 px control corner, a 500-weight label at 13, 14 or 16 px, and an 8 px gap beside its icons. The call to action fills with the accent, destructive with the status red, both under a lit top edge; secondary is the quiet well with a boundary, outline the boundary alone, ghost and link bare (the link underlined). A filled button brightens and rises 1 px under the pointer over 150 ms and settles back when it leaves; the other looks take the well's wash at once. A press scales the button to 96% and darkens it, unless steadyPress. A disabled button fades to 40%; a loading one keeps its full look while its spinner runs. A disabled or loading button stays put, and Reduce Motion makes the lift instant. On iOS the button is the 50 pt capsule with a semibold 17 pt label, on Android the 40 dp Material 3 stadium, both in the same 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.
softHover dims a link to 85% on hover in place of the wash. Inverse actions use a translucent white hover wash over a branded surface.

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).
<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>

Workflow actions

Use outline quiet for a muted secondary operation and outline inverse on a dark branded surface. consistent gives short workflow actions a shared minimum width; comfortable supplies the roomier surround used for a compact featured action.

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).

Tokens

Read from the playground render. Values follow the scheme and the ion hue.
--bls
--bw
--fd
--ft
--ion
--ion-fg

Related components