Variant × size matrix
Every variant crossed with every size — the complete default (idle) state. Rows are variants, columns are sizes. Heights step 7 → 10 → 11 (sm → md → lg); the visual weight steps primary → secondary → ghost, with danger held apart for destructive actions.
| variant \ size | sm | md | lg |
|---|---|---|---|
| primary | |||
| secondary | |||
| ghost | |||
| danger |
- size="sm"
- h-7, px-3, text-xs — dense toolbars / inline actions.
- size="md"
- h-10, px-4, text-sm — the default (defaultVariants.size).
- size="lg"
- h-11, px-5, text-sm — prominent primary actions / hero CTAs.
Variants on surface vs background
Each variant shown at the default md size on both bg-surface (cards/panels) and bg-background (the page canvas). Ghost and secondary lean on surface contrast, so they read differently on each — always check the canvas a button will actually live on.
Solid bg-[--color-accent-strong] with text-[--color-on-accent], hover brightens, transparent border, a subtle shadow-sm. The one main action per view.
bg-surface-2 / hover:bg-surface-3, primary text, a visible --color-border edge. The neutral companion action.
transparent fill / hover:bg-surface-2, muted text, no border. Low-emphasis / tertiary actions.
bg-bad / hover:brightness-110, text-on-danger, transparent border. Reserved for destructive actions only — red is never decorative.
Sizes
The three sizes side by side at the primary variant. Size changes height, horizontal padding and font size only — never the color, weight or radius. Keep one size consistent within a button group.
With a leading icon
Drop a lucide icon as the first child — the component's built-in gap-2 spaces it from the label. Use h-4 w-4 to match the md/lg type size, and mark a purely decorative icon aria-hidden (the text already names the action). For a trailing icon, put it after the label.
- gap-2
- Built into the base class — no manual margin needed between icon and label.
- icon size
- Use h-4 w-4 to align with sm/md/lg label text; the spinner shown while loading is also h-4 w-4.
- aria-hidden
- Decorative icons get aria-hidden="true" so screen readers announce only the label. For an icon-only button, add an aria-label instead.
Loading
loading renders a spinning Loader2 (h-4 w-4, animate-spin) before the children and disables the button so it can't be re-submitted. Keep the label in place so the control doesn't change width — the spinner is added, not swapped in.
- loading={true}
- Prepends the animate-spin Loader2 and sets the underlying <button> disabled (disabled = disabled || loading).
- label stays
- Children render alongside the spinner — keep the text (e.g. “Saving…”) so width is stable and the action stays legible.
- accessibility
- Because the button is disabled while loading it can’t be activated again; pair with aria-live elsewhere if you need to announce completion.
Disabled
disabled drops the button to 50% opacity, switches the cursor to not-allowed and removes hover feedback (the disabled:* utilities live in the base class). Each variant idle → disabled is paired below so the muting is obvious.
- disabled
- The <Button> component renders a truly-disabled button as a neutral, clearly-inert surface (muted bg + faint text, no shadow); raw buttonVariants() consumers fall back to the base disabled:opacity-50. A loading button keeps its variant color instead.
- disabled:cursor-not-allowed
- Cursor feedback that the control is inert; hover background changes are also suppressed.
- loading ⊃ disabled
- A loading button is always disabled too — you don’t need to pass both.
Full width
There is no fullWidth prop — pass w-full through className. The button is inline-flex with centered content, so it stretches to its container and keeps its label centered. Common in narrow forms, mobile sheets and card footers.
- w-full
- The full-width recipe — there is no dedicated prop. className is tailwind-merged last so it wins over the base.
- centered content
- Base class is inline-flex items-center justify-center, so the label/icon stay centered when stretched.
Pairing & emphasis
One primary per view carries the main action; pair it with a secondary or ghost for the lesser action, and reserve danger for genuinely destructive operations. The solid accent fill on primary is what makes it read as the lead — don't promote a second button to primary in the same group.
- primary = solid accent
- Only the primary variant carries the solid accent fill + shadow-sm. It is the cue for the single lead action — keep it to one per group.
- secondary / ghost
- The companion actions. secondary for a real alternative (Save draft), ghost for the quietest escape (Cancel).
- danger is reserved
- Red (bg-bad) is destructive-only — never use the danger variant for emphasis. Give a destructive action a non-destructive escape (ghost).
- never color alone
- Meaning is carried by the label (and often an icon), not the hue — danger still says “Delete”.