Skip to content
HeyOz
Components

Button

Eight button types, icon buttons and links, in seven sizes, squircle and pill, with their states.

Source
components/button.json
Figma
Build by hand: see Build it in Figma
In code
<Button size shape icon>

Playground

Pick any type, size, shape, content and state. Sizes keep their real proportions; zoom with − and +. Anatomy marks every part with its token.

300%
52643178
Type
Size
Content
Shape
State
Anatomy
  1. 1
    Frame · fill/secondary · border/secondary · stroke/1Auto layout, horizontal. Carries fill, border, radius, inner shade.
  2. 2
    Leading icon frame · 16 frame · 12 vector · size/icon/smSquare, from the icon scale by text size (16, 20 or 24). The icon grid is drawn inside: the vector fills the key square (75%), so at least 1/8 of the frame stays clear. Optional.
  3. 3
    Label · body/md/medium · 14/20Text style body/{size}/medium. Buttons use the body role only.
  4. 4
    Trailing icon frame · 16 frame · 12 vectorSame as the leading one. Optional; becomes the spinner when loading.
  5. 5
    Padding x · 12px · space/12Left and right. Top and bottom are not set: the height is fixed and children are centred.
  6. 6
    Gap · 4px · space/4Between every child.
  7. 7
    Height · 32px · size/control/xssize/control/{size}, fixed.
  8. 8
    Radius · 12px · 32 × 0.375, to the nearest 4Squircle: height × 0.375, rounded to the nearest 4. Pill: radius/full.

Types

Eight types. Every type has the same sizes, shapes and states; only the colours change. Shown at xs, the everyday size.

Typeresthoverpresseddisabledloading
brandThe main action. One per view.
primaryStrong neutral action, light on dark.
secondaryDefault action on surfaces.
tertiaryQuiet action; chips and settings in the prompt box.
outlineBorder only, no fill.
ghostNo fill or border until hovered.
brand-toneBrand action that should not shout.
brand-ghostBrand text, fill only on hover.

Icon buttons

The same eight types, icon only: square, same height, same icon frame. Pills become circles.

Typeresthoverpresseddisabledloading
brand
primary
secondary
tertiary
outline
ghost
brand-tone
brand-ghost

Sizes

Seven sizes, one per size/control token. Each comes as a squircle and as a pill, with the same padding. Icon-only buttons are square at the same height.

SizeSquirclePillIcon only
2xssize/control/2xs
xssize/control/xs
smsize/control/sm
mdsize/control/md
lgsize/control/lg
xlsize/control/xl
2xlsize/control/2xl

Build it in Figma

Eight steps, drawn at 3× for xs. Every other size is the same structure with the values from the Size spec.

label
1 · Label
  • Text layer "label".
  • Text style body/md/medium (14/20). Fill content/primary.
  • Resizing: hug width, hug height.
label
2 · Icon frames
  • Frame 16 × 16 (size/icon/sm), chosen by text size. No fill. Clip content on.
  • Use the icon component as is: it is drawn on the icon grid, the vector filling the key square, 12 × 12 centred. Constraints Scale.
  • Icon colour content/primary. One on each side of the label.
label
3 · Auto layout
  • Select icon, label, icon → Shift + A.
  • Direction horizontal. Gap 4 (space/4).
  • Alignment: centre (middle-centre).
label
4 · Size the frame
  • Height fixed 32 (size/control/xs). Width hug.
  • Padding left and right 12 (space/12).
  • Padding top and bottom 0: the fixed height and centring do the rest.
5 · Style the frame
  • Fill fill/secondary. Stroke border/secondary, 1, inside.
  • Radius 12 (32 × 0.375, to the nearest 4). Pill: 1000.
  • Effect: inner shadow x 0, y −4, blur 10, #000 50%.
6 · Make it a component
  • Ctrl + Alt + K. Name it Button.
  • Boolean properties: Leading icon, Trailing icon, Label. Instance swap on both icons.
  • Text property on the label.
7 · Variants
  • Properties: Size (2xs … 2xl), Shape (squircle, pill), State (rest, hover, pressed, disabled, loading).
  • Per size: change height, padding x, gap, text style, icon frame and radius from the Size spec table.
  • Hover / pressed: fill secondary-hover / secondary-active; stroke and icons border/brand. Disabled: the *-disabled set. Loading: disabled + trailing icon swapped for the spinner.
8 · Icon only
  • Label property off, trailing icon off.
  • Width fixed = height, padding 0. The icon stays centred.
  • Pill shape gives a circle.

Size spec

Source: components/button.json. Every value you type in Figma is on the 4px grid. In Figma: auto layout, horizontal, height fixed, children centred; padding y is not set, it is what is left over.

Size
Height
Padding x
Padding y
Gap
Label
Icon frame
Radius
2xs
28pxsize/control/2xs
10pxset, not a token
6pxleft over
4pxspace/4
12 / 16body/sm/medium
16 × 16size/icon/sm
10pxset
xs
32pxsize/control/xs
12pxspace/12
6pxleft over
4pxspace/4
14 / 20body/md/medium
16 × 16size/icon/sm
12pxheight × 0.375 → 4
sm
40pxsize/control/sm
16pxspace/16
10pxleft over
4pxspace/4
14 / 20body/md/medium
16 × 16size/icon/sm
16pxheight × 0.375 → 4
md
48pxsize/control/md
20pxspace/20
12pxleft over
8pxspace/8
16 / 24body/lg/medium
20 × 20size/icon/md
20pxheight × 0.375 → 4
lg
56pxsize/control/lg
24pxspace/24
14pxleft over
8pxspace/8
18 / 28body/xl/medium
24 × 24size/icon/lg
20pxheight × 0.375 → 4
xl
64pxsize/control/xl
32pxspace/32
18pxleft over
8pxspace/8
20 / 28body/2xl/medium
24 × 24size/icon/lg
24pxheight × 0.375 → 4
2xl
72pxsize/control/2xl
40pxspace/40
20pxleft over
12pxspace/12
24 / 32body/3xl/medium
32 × 32size/icon/xl
28pxheight × 0.375 → 4

Pill: same values, radius/full (1000). Inner shade on every filled state, inset 0 -4 10 rgba(0,0,0,.5). A Figma effect, not a token (D-021).

Colour

Every type, state by state. none = transparent. Every state with a fill gets the inner shade (0, −4, blur 10, #000 50%). Disabled colours; the trailing icon becomes a spinner. The spinner is an icon: same frame, the ring fills the key square, line weight = frame × size/icon/grid/stroke. Rest colours plus a ring outside the border: border/focus, stroke/2.

Type
State
Fill
Border
Label
Icon
Inner shade
brand
rest
fill/brand
none
content/on-brand
content/on-brand
yes
hover
fill/brand-hover
none
content/on-brand
content/on-brand
yes
pressed
fill/brand-active
none
content/on-brand
content/on-brand
yes
disabled
fill/brand-disabled
none
content/on-brand-disabled
content/on-brand-disabled
yes
primary
rest
fill/inverse
none
content/inverse-primary
content/inverse-primary
yes
hover
fill/inverse-hover
none
content/inverse-primary
content/inverse-primary
yes
pressed
fill/inverse-active
none
content/inverse-primary
content/inverse-primary
yes
disabled
fill/secondary-disabled
none
content/primary-disabled
content/primary-disabled
yes
secondary
rest
fill/secondary
border/secondary
content/primary
content/primary
yes
hover
fill/secondary-hover
border/brand
content/primary
border/brand
yes
pressed
fill/secondary-active
border/brand
content/primary
border/brand
yes
disabled
fill/secondary-disabled
border/secondary-disabled
content/primary-disabled
content/primary-disabled
yes
tertiary
rest
fill/tertiary
border/tertiary
content/primary
content/primary
yes
hover
fill/tertiary-hover
border/brand
content/primary
border/brand
yes
pressed
fill/tertiary-active
border/brand
content/primary
border/brand
yes
disabled
fill/tertiary-disabled
border/tertiary-disabled
content/primary-disabled
content/primary-disabled
yes
outline
rest
none
border/secondary
content/primary
content/primary
no
hover
fill/secondary
border/brand
content/primary
border/brand
yes
pressed
fill/secondary-hover
border/brand
content/primary
border/brand
yes
disabled
none
border/secondary-disabled
content/primary-disabled
content/primary-disabled
no
ghost
rest
none
none
content/primary
content/primary
no
hover
fill/secondary
border/brand
content/primary
border/brand
yes
pressed
fill/secondary-hover
border/brand
content/primary
border/brand
yes
disabled
none
none
content/primary-disabled
content/primary-disabled
no
brand-tone
rest
fill/brand-secondary
none
content/brand
content/brand
yes
hover
fill/brand-secondary-hover
none
content/brand
content/brand
yes
pressed
fill/brand-secondary-active
none
content/brand
content/brand
yes
disabled
fill/brand-secondary-disabled
none
content/brand-disabled
content/brand-disabled
yes
brand-ghost
rest
none
none
content/brand
content/brand
no
hover
fill/brand-secondary
none
content/brand
content/brand
yes
pressed
fill/brand-secondary-hover
none
content/brand
content/brand
yes
disabled
none
none
content/brand-disabled
content/brand-disabled
no