Skip to content

Built · v1.0 · Figma set

Button

Buttons are used for interface actions. The primary style marks the main call-to-action, one per screen. How buttons share a screen (hierarchy, pairing, dialogs) lives in Guidelines → Action hierarchy; this page covers the component itself.

Use when

  • Moving a flow forward. Submit, save, confirm.
  • Triggering an action on the current screen.
  • Closing or confirming inside a dialog (tertiary / text emphasis).

Don’t use when

  • Navigating to another place. Use a text link.
  • Signing in with a provider. Use Button/Social (separate set, Apple HIG rules).
  • Floating or segmented actions. FAB and plan chips are independent components, not button variants.

Five layers, mirroring how M3 builds its own buttons. The outer Component is the touch target. It never shrinks below 44 px even when the visible button does.

Placeholder, 720x1100. A tall visual against a long section: this is the well-behaved case.
12icon slot4Save5
  1. Component. Transparent touch target, fixed height.
  2. Container. The visible button: fill token + radius/full.
  3. State layer. Layout only: padding + icon-label gap, no fill.
  4. Icon. Instance-swap slot, hidden by default.
  5. Label. Text node, bound to the Label text property.

Type sets emphasis. Primary, Secondary, Tertiary. Which one to pick, and how they combine, is defined once in Action hierarchy; don’t re-decide it per screen.

Placeholder, 900x520. Wide and short, so the prose runs past it.

Size: three sizes, and the numbers only make sense in pairs. The three-layer architecture means what you touch and what you see are different boxes: the outer Component is the touch target, the Container is the visible pill, and the State Layer in between holds the padding. Every value below is measured off the built set.

DefaultButtontouch 48 · pill 48 · padding 12 / 24 · Inter Bold 16 / 24
SmallButtontouch 44 · pill 38 · padding 10 / 16 · Inter Medium 13 / 18
XSmallButtontouch 44 · pill 26 · padding 5 / 16 · Inter Regular 12 / 16

The dashed box is the outer Component, drawn at its real size. It is invisible in the product; it is what a finger lands on.

The pill height is not a value anyone typed. It falls out of line-height plus vertical padding: 24 + 12 + 12 = 48, 18 + 10 + 10 = 38, 16 + 5 + 5 = 26. Change the label style and the button resizes itself, which is the point.

Only Default has the pill and the touch target at the same height. Small carries 3 px of invisible buffer above and below, XSmall carries 9 px. That is what keeps both of them at the 44 px WCAG 2.5.5 minimum while still looking small.

Horizontally the same thing happens, and it catches people out. The outer Component has a min-width of 110, so a short label leaves invisible space on both sides:

SizePill width with “Button”OuterInvisible per side
Default1011104.5
Small7411018
XSmall7011020

So two XSmall buttons set flush against each other read as 40 px apart. If a row of small buttons looks too airy, this is why: the gap is the touch target doing its job, not a spacing mistake.

State: Enabled and Disabled are the only Figma variants. Hover, focused and pressed are not designed as variants: Flutter applies MaterialState overlays (8% / 12% / 12%) on top of the base tokens.

Width: hug content by default. The outer Component clamps it between 110 and 320, both measured off the set: below 110 it stops shrinking, above 320 it stops growing. To stretch a CTA manually, switch the Container from Hug to Fill; it keeps its visual padding because the padding lives on the State Layer.

Do
Start free trial
Always radius/full. The pill is the shape of the system.
Don’t
Start free trial
Never rectangular corners. The legacy app shipped 44 forms of its main button. The pill replaces all of them.
  • Always pick the size by context, not taste: Default for screen CTAs, Small inside cards and list rows, XSmall only in dense trailing slots.
  • Add an icon only when it carries meaning the label can’t (Show icon is off by default).
  • Never build an icon-only button from this component. That slot has no DS answer yet.

Color comes only from the four type tokens. Never restyle a button locally. A destructive action keeps its type and takes feedback/error as fill; don’t invent a “danger” variant. In dark modes the primary resolves from yellow to lime automatically via brand/action.

  • Buttons are fluid: they stretch with their container (FILL), they don’t own a width.
  • At the 360 × 667 floor, two actions that don’t fit one row stack. Primary on top (Action hierarchy).
  • Disabled swaps tokens (button/disabled + button/onDisabled). Never fade with opacity.
  • Lead with a verb, sentence case, no punctuation: Save changes, not SAVE or Save.
  • The label must make sense out of context, Delete account, never Yes or OK.
  • Keep it under ~3 words; if the label needs more, the action is unclear upstream.

The touch target is carried by the outer Component layer: 48 px on Default, and a fixed 44 px buffer on Small/XSmall even though the visible Container is 38 / 26 px. Don’t strip it when detaching. Disabled state is conveyed by the button/disabled fill and button/onDisabled text pair, not by opacity, so it survives high-contrast settings. Every instance must have a Label text value that describes the action on its own; the icon slot is decorative and never replaces the label.

The parity contract. 8 tokens, resolved per mode (Light / Carbon / Navy).

Placeholder, 760x760.
button/primary → brand/actionbutton/secondary → Cream/200button/tertiarybutton/disabledbutton/onPrimarybutton/onSecondarybutton/onTertiarybutton/onDisabled

Plus radius/full and spacing 24 / 16 / 12 / 10 / 8 / 5 for padding and icon gap.