Last update: August 2026. All opinions are my own.

Web Development · Post 11/15

Every button in your app is a combination of a variant (how important is this action?) and a state (what's happening right now?). If you can draw the 3×5 grid, you've designed the whole button system.

The three variants — and only three

Button variants — primary (solid, accent colour), secondary (outlined), tertiary (text only), with an icon-only version. Button anatomy and variations guide — the parts of a button (icon, label, indicator) and every common variation shown in one grid.
  • Primary — solid accent colour. The one action on the page you want the user to take. There should be one primary button per view.
  • Secondary — outlined or ghost. Actions that are important but not the action. Cancel, back, secondary CTAs.
  • Tertiary — text-only, no border. Low-emphasis actions. "Learn more," "Skip for now."

That's it. If you find yourself designing a fourth variant, you're probably solving the wrong problem.

The five states — and why they all matter

Every interactive element goes through five states. If your button doesn't visually distinguish all five, users don't know if the click registered.

  • Default — resting state.
  • Hover — mouse is over it (desktop only, but design for it anyway).
  • Focus — keyboard has selected it. This is the accessibility one people skip and shouldn't.
  • Active — being pressed right now (mouse down, key down).
  • Disabled — can't be clicked. Lower contrast, cursor: not-allowed.

For a primary button, that's usually:

.button-primary {
  background: var(--color-primary);         /* default */
}
.button-primary:hover {
  background: var(--color-primary-hover);   /* slightly darker */
}
.button-primary:focus-visible {
  outline: 2px solid var(--color-focus);    /* keyboard focus ring */
  outline-offset: 2px;
}
.button-primary:active {
  background: var(--color-primary-active);  /* darker still */
  transform: translateY(1px);               /* optional press-down */
}
.button-primary:disabled {
  background: var(--color-neutral-300);
  color: var(--color-neutral-500);
  cursor: not-allowed;
}

Notice :focus-visible (not just :focus) — this shows the ring for keyboard users but hides it for mouse clicks. It's the modern way.

The vocabulary lives here

Buttons are one part of a bigger vocabulary — inputs, links, chips, toggles all share the variant + state pattern. Once you build one, you've built the mental model for all the others:

UI vocabulary overview — buttons, inputs, links, chips, toggles, and their variants and states side by side.

Size scale

Same principle as everything else — pick a small scale, stick to it. Three sizes is enough for most apps:

  • sm — 32px tall, 12px horizontal padding. Toolbars, table row actions.
  • md — 40px tall, 16px horizontal padding. The default. Almost everything.
  • lg — 48px tall, 20px horizontal padding. Marketing pages, empty-state CTAs.

Anything smaller than 32px hits the touch target minimum (Post 4). Don't make micro-buttons unless you're absolutely sure no thumb will ever touch them.

Icon buttons

An icon-only button (a trash icon, a search icon) needs:

  • Minimum 44×44px hit area — even if the icon is 16×16, pad it out.
  • An aria-label — the icon isn't text, screen readers need a name for it.
  • A tooltip — for sighted users who don't know your icons yet.

Icons are ambiguous. The trash icon has meant "delete," "archive," and "move to a folder" on different apps. Assume the reader has to guess and give them a way to check.


Next up — Post 12: Forms — anatomy and states.