primary
The one main action on the screen. Filled and loud so the eye lands on it first.
Avoid: two primaries fighting in one view.
Use this when a single component ships in several variants and sizes, and the reader needs to see them side by side and learn which to reach for.
Copyable exemplar. Lift the <section> blocks below into a lesson built from assets/lesson-template.html — keep the design tokens and the .tech-toggle pattern verbatim.
A Button is one component, but it wears different outfits. A bold filled button shouts "do this"; a quiet outlined one says "maybe this"; a barely-there text button whispers "or this"; and a red one warns "careful." On top of that, each comes in three sizes. The matrix below lays out every outfit at once so you can compare them and pick the right one on purpose — not by accident.
Think of it like… traffic signs. The same sign shape carries different meaning by color and size: a big red STOP, a modest yield, a small advisory. You read intent at a glance — and you'd never paint a STOP sign green.
The Button takes two orthogonal props: variant (primary | secondary | ghost | danger) drives color/emphasis, and size (sm | md | lg) drives padding and font-size. They compose: any variant works at any size, giving a 4×3 grid from a single CSS class set. Keeping the axes independent is what lets a design system stay small — you author 4 + 3 = 7 rules, not 12 bespoke buttons.
Exactly one primary per view is the rule of thumb: emphasis only reads as emphasis when it's scarce. danger never relies on red alone (it carries a verb like "Delete" and, ideally, a confirm step). Every variant keeps a visible :focus-visible ring for keyboard users, and color contrast is checked against the variant's own background, not the page.
Read it like a grid: each row is a variant, each column is a size. The cell shows the real, live button at that combination — these are the same .btn elements the app renders.
| variant \ size | sm | md | lg |
|---|---|---|---|
| primary | |||
| secondary | |||
| ghost | |||
| danger |
The bottom-right cell of the table is just:
// React <Button variant="danger" size="lg">Delete account</Button> // rendered HTML <button class="btn danger lg" type="button">Delete account</button>
Sizes change only padding and font-size; variants change only color and border. Because the two axes never touch, you can drop a new size="xl" without re-checking any variant.
The matrix shows what each looks like. These cards say when to use each one. Pick a size to preview every variant at that size; the matching row lights up.
primary
The one main action on the screen. Filled and loud so the eye lands on it first.
Avoid: two primaries fighting in one view.
secondary
The supporting action that sits beside primary — "Cancel", "Back". Outlined, quieter.
Avoid: using it for the action you actually want clicked.
ghost
The low-priority option — "Skip", toolbar icons, links-that-act. Almost invisible until hovered.
Avoid: on a critical or destructive action (too easy to miss).
danger
The destructive action — delete, remove, revoke. Red plus a clear verb plus a confirm step.
Avoid: red alone with vague copy like "OK".
/* base + two independent axes */ .btn { font-weight:600; border-radius:8px; border:1.5px solid transparent; } /* size axis */ .btn.sm { font-size:13px; padding:7px 13px; } .btn.md { font-size:15px; padding:10px 18px; } .btn.lg { font-size:17px; padding:13px 24px; } /* variant axis */ .btn.primary { background:#D97757; color:#fff; } .btn.secondary { background:#fff; border-color:#D1CFC5; } .btn.ghost { background:transparent; color:#B85C3E; } .btn.danger { background:#B04A3F; color:#fff; }
The size picker above swaps only the size class on each demo button via JS — proof the axes are independent at runtime.