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
Section titled “Use when”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.
Anatomy
Section titled “Anatomy”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.
- Component. Transparent touch target, fixed height.
- Container. The visible button: fill token +
radius/full. - State layer. Layout only: padding + icon-label gap, no fill.
- Icon. Instance-swap slot, hidden by default.
- Label. Text node, bound to the
Label textproperty.
Variants
Section titled “Variants”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.
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.
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.
The slack is the accessibility
Section titled “The slack is the accessibility”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:
| Size | Pill width with “Button” | Outer | Invisible per side |
|---|---|---|---|
| Default | 101 | 110 | 4.5 |
| Small | 74 | 110 | 18 |
| XSmall | 70 | 110 | 20 |
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.
Design guidelines
Section titled “Design guidelines”Do / Don’t
Section titled “Do / Don’t”radius/full. The pill is the shape of the system.- 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 iconis off by default). - Never build an icon-only button from this component. That slot has no DS answer yet.
Visual style
Section titled “Visual style”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.
Behavior
Section titled “Behavior”- 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.
Editorial
Section titled “Editorial”- 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.
Accessibility
Section titled “Accessibility”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.
Tokens
Section titled “Tokens”The parity contract. 8 tokens, resolved per mode (Light / Carbon / Navy).
Plus radius/full and spacing 24 / 16 / 12 / 10 / 8 / 5 for padding and icon gap.