ComponentsButton

Button

atom

The 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;
}