ComponentsButton
Button
atomThe primary action control — one loud primary per region, quiet siblings, a locked height scale.
Buttons
One loud primary, quiet siblings. Variant · size · state — all at intrinsic width.
Best practices
Do
- One .btn--primary per view region — everything else stays quiet (default or .btn--ghost).
- A destructive action wears .btn--danger, not primary.
- Use the size modifiers (.btn--sm / .btn--xs) — heights are locked to the control scale.
Don't
- Don't stretch or restyle a button's height with padding overrides — the height invariant keeps every control row aligned.
- Don't put two primaries side by side; demote one to ghost.
3
Best practices
Do
- One .btn--primary per view region — everything else stays quiet (default or .btn--ghost).
- A destructive action wears .btn--danger, not primary.
- Use the size modifiers (.btn--sm / .btn--xs) — heights are locked to the control scale.
Don't
- Don't stretch or restyle a button's height with padding overrides — the height invariant keeps every control row aligned.
- Don't put two primaries side by side; demote one to ghost.
Recipe CSS
This is the exact CSS your kit ships for Button — token-driven, so it re-themes with every knob. Get it (and the rest) from the configurator's Use kit panel.
/* === Buttons ===
Horizontal padding uses max() against --k-radius-md so it scales when
the user picks a larger radius. At pill (md=26px) the curve dominates
the visual width, so we expand padding to keep the label clear of the
curve — a best-practice rule of thumb: padding-inline >= radius * 0.75. */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--k-s-6);
/* Default button height is Scale-driven and equals the input height per tier:
* Compact → 32px · Default → 36px · Comfortable → 40px (shadcn h-8/9/10 family).
* Padding-block is 0 because min-height + flex centering handles vertical
* positioning, and removing padding-block keeps the height exact. */
min-height: var(--k-btn-h, var(--k-btn-h-default, 36px));
padding-block: 0;
/* Horizontal padding scales with TWO dimensions:
* 1. Button height (Scale drives 32/36/40px) — taller buttons need
* more side padding to stay optically balanced. height × 0.4
* matches Apple HIG / Material defaults.
* 2. Pill detection — when --k-radius-button is huge (999px), the
* curve eats horizontal space, so we add a pill-only bump
* proportional to height (≈ ×0.55) so the label clears the curve.
* Clamped to 0.55×height to keep pill+bold from going absurd.
* The 14px floor + radius-md fallback stay as safety nets. */
/* --btn-r: the radius THIS button family uses. Solid buttons (primary/
secondary/danger) follow the button-radius token; ghost controls override
it to the box radius below — they read as fields / quiet toolbar controls,
not buttons, so "Button radius" shouldn't reshape them. One var drives BOTH
the corner and the pill-aware padding so they can never disagree. */
/* Comp-tier hook (H2 lazy override): --k-btn-radius is NEVER defined by
default — zero bytes until a consumer/agent sets it ("square corners on
just my buttons"). Same pattern on the other hooks below. */
--btn-r: var(--k-btn-radius, var(--k-radius-button, var(--k-radius-md)));
padding-inline: max(
14px,
calc(var(--k-radius-md) * 0.75),
calc(var(--k-btn-h-default, 34px) * 0.4),
min(
calc(var(--btn-r) * 0.5),
calc(var(--k-btn-h-default, 34px) * 0.55)
)
);
border-radius: var(--btn-r);
font-size: var(--k-type-small);
font-weight: var(--k-ui-weight, 600);
/* line-height: 1 collapses the text line-box to exactly its font-size,
so the label centers cleanly against icons via align-items: center.
Without this, inherited 1.15-1.5 line-heights make the text box taller
than the icon, and the visual baseline of glyphs drifts a pixel or two
above/below the icon's optical center. */
line-height: 1;
text-transform: var(--k-ui-transform, none);
letter-spacing: var(--k-ui-tracking, 0);
font-family: var(--k-font-body);
/* Unified motion — every button animates bg + transform + shadow + filter
* on the same tokens, so hover-lift and the click-press feel consistent
* across primary/secondary/ghost/outline/link/icon. */
transition:
background var(--k-dur, 200ms) var(--k-ease, ease),
transform var(--k-dur-fast, 120ms) var(--k-ease, ease),
box-shadow var(--k-dur-fast, 120ms) var(--k-ease, ease),
filter var(--k-dur-fast, 120ms) var(--k-ease, ease),
border-radius var(--k-dur-fast, 120ms) var(--k-spring, var(--k-ease, ease));
border: max(1px, var(--k-bw)) solid transparent;
cursor: pointer;
}
/* Unified press — what :active does: a 0.96 scale squish (the house feel).
* Generic so ghost/outline/link get it too. */
.btn:active:not(:disabled) {
transform: scale(0.96);
}
/* SVG icons inside buttons should never shrink and should render as a
* standalone flex item, not as inline-text-baseline content. Most preview
* usage already passes display:block via Lucide, but explicit-here guards
* against fonts/icons that use vertical-align tricks. */
.btn > svg {
flex-shrink: 0;
display: block;
}