Button.
One action, one label, one place to press. Always an imperative verb plus a noun, never the verb alone. Type carries the meaning, style carries the weight, size carries the context — the three axes are independent, and every combination is a real button.
Try it
Type
Four semantic tokens. Style and size below change how much weight each one carries, not what it means.
primaryThe one action you want people to take on this view. One per view.containers/primaryneutralCancel, back, secondary paths. Pair it with a Primary, never with another Neutral Filled.containers/neutralsuccessConfirm, approve, complete. Reserve it for the positive final step.containers/successdangerDelete, remove, reset. Always pair with a confirmation and a plain-language warning.containers/dangersecondary and destructive still work as aliases for neutral and danger — nothing built against the old API breaks. subtle still works too, and now forces appearance="text".
Options
Type carries the meaning, size carries the context, state carries the feedback. Exact values live in Specification below.
Style
Filled is a decision. Outlined and Text are lighter ways to say the same type when a Filled button would out-shout everything near it.
One Filled per view, always. A screen with three Filled buttons has no primary action — it has three. Outlined pairs with a Filled sibling; Text stands alone in toolbars and dense rows where a border would add noise.
Size
Heights come from input/height — a Button always lines up with the field or row next to it.
28px38px48pxStates
Default → Hover → Pressed → Disabled ship today. Focus is specified below but is not a variant — it is what :focus-visible always draws.
not a variantIcons
Placement and meaning.
Specification
Every value is a token. If a value is not in this table, it does not exist.
Anatomy, icon placement, do and don't rules with live examples, and the accessibility notes above. Everything about using the button well lives on this page.
Accessibility
Props (React)
type ButtonProps = {
variant?: "primary" | "neutral" | "success" | "danger";
// legacy aliases still accepted: "secondary" → neutral, "destructive" → danger,
// "subtle" → neutral text (forces appearance="text")
appearance?: "filled" | "outlined" | "text"; // default "filled"
size?: "s" | "m" | "l"; // default "m" — 28 / 38 / 48px
icon?: IconName; // optional icon, 16px
trailing?: boolean; // render icon after the label instead of before
disabled?: boolean;
pressed?: boolean; // toggle state — adds aria-pressed
children: ReactNode; // verb + noun, please
onClick?: (e) => void;
};