Sizing

Cladd is a dense UI kit. The size scale is tuned for information-rich screens where you need a lot of controls on one surface and they all have to line up. The scale itself is small — seven steps from 2xs to 2xl — but the rules for which size to use and how sizes nest are the part that's worth memorising.

This page covers the scale, the per-component height reference, the root vs nested ramps, the density guidance (default md, when to go smaller, when to go larger), and the nesting rule that makes a <Chip> inside a <Button> automatically read at the right proportion. For per-component prop reference, see each component's own page.

The scale

Every interactive control with a size prop draws from the same seven-step ramp:

2xs   xs   sm   md   lg   xl   2xl

The full scale is implemented by Button, Chip, Shortcut, and Spinner. Components that don't make sense below a certain density — Input, NumberField, Textarea, OTPField — start at sm and skip the two smallest steps. A few of the smallest primitives (Checkbox, Radio, Switch, Slider) only ship sm and md — by design, they don't need more.

2xs
xs
sm
md
lg
xl
2xl
{ALL_SIZES.map((size) => (
  <div key={size} className="flex flex-col items-center gap-2">
    <Button size={size} className="w-12">
      {size}
    </Button>
    <span className="font-mono text-xs text-cladd-fg-soft">{size}</span>
  </div>
))}

Heights

The two CSS spacing ramps that drive most cladd heights are --spacing-cladd-{size} (root) and --spacing-cladd-nested-{size} (nested = root minus 8 px). Every sized component on the seven-step scale picks one of these two ramps.

SizeRoot heightNested height
2xs16 px
Button
8 px
Chip, Shortcut, Spinner
xs20 px
Button
12 px
Chip, Shortcut, Spinner
sm24 px
Button, Input, NumberField, Textarea, OTPField
16 px
Chip, Shortcut, Spinner
md28 px
same set as sm
20 px
Chip, Shortcut, Spinner
lg32 px
same set as sm
24 px
Chip, Shortcut, Spinner
xl40 px
same set as sm
32 px
Chip, Shortcut, Spinner
2xl48 px
same set as sm
40 px
Chip, Shortcut, Spinner

The "Root height" column is the height of Button, Input, and friends at that size — the full-size controls. The "Nested height" column is the height of Chip, Shortcut, and Spinner at the same size token — exactly 8 px shorter, so they fit cleanly inside a root-ramp row.

Checkbox, Radio, Switch, and Slider don't appear in this table — they use a separate, smaller two-step scale covered in Two-size primitives below.

Root vs nested

The same size token produces two different heights depending on which ramp the component reads from. Root-ramp controls fill a row; nested-ramp controls are designed to sit inside a root-ramp control without overflowing or feeling cramped.

sm
Chip
CTRL
K
md
Chip
CTRL
K
lg
Chip
CTRL
K
xl
Chip
CTRL
K
{(['sm', 'md', 'lg', 'xl'] as ButtonSize[]).map((size) => (
  <div key={size} className="flex items-center gap-4">
    <span className="w-12 font-mono text-xs text-cladd-fg-soft">
      {size}
    </span>
    <Button size={size}>Button</Button>
    <Chip size={size}>Chip</Chip>
    <Shortcut size={size}>cmd K</Shortcut>
    <Spinner size={size} />
  </div>
))}

When you place a Chip, Shortcut, or Spinner inside a Button (or any other root-ramp control), passing the same size to both is the right call — the nested component will automatically render 8 px shorter, with matching padding and font-size, and sit perfectly in the row.

md
lg
xl
<div className="flex items-center gap-3">
  <span className="w-12 font-mono text-xs text-cladd-fg-soft">md</span>
  <Button size="md">
    Save
    <Chip size="md" color="green">
      12
    </Chip>
    <Shortcut size="md">cmd S</Shortcut>
  </Button>
</div>
<div className="flex items-center gap-3">
  <span className="w-12 font-mono text-xs text-cladd-fg-soft">lg</span>
  <Button size="lg">
    Save
    <Chip size="lg" color="green">
      12
    </Chip>
    <Shortcut size="lg">cmd S</Shortcut>
  </Button>
</div>
<div className="flex items-center gap-3">
  <span className="w-12 font-mono text-xs text-cladd-fg-soft">xl</span>
  <Button size="xl">
    Save
    <Chip size="xl" color="green">
      12
    </Chip>
    <Shortcut size="xl">cmd S</Shortcut>
  </Button>
</div>

You don't need to pick a "Chip is one step smaller than Button" mapping yourself. The scale is the contract: size="md" means "the md row" — whichever ramp the component reads.

A dense row

When everything in a row shares the same size token, controls line up automatically — root-ramp items at the row height, nested-ramp items at the inner height. This is how cladd toolbars, filter bars, and editor headers work without per-item adjustments.

Draft
CTRL
S
<div className="flex flex-wrap items-center gap-2">
  <Button size="md">Save</Button>
  <Input size="md" placeholder="Title" className="w-40" />
  <Chip size="md" color="blue">
    Draft
  </Chip>
  <Shortcut size="md">cmd S</Shortcut>
  <Spinner size="md" />
</div>

The pattern: pick a row size (almost always md), pass it to every sized component, done.

Density guidance

Cladd is already dense by default — a 28 px Button is smaller than what most kits ship as their default. So the rule of thumb is:

  • Default to md for most application UI. Toolbars, header rows, list rows, inspector panels.
  • Use lg for prominent primary actions and the main Input row of a form. Input and NumberField default to lg for exactly this reason — the floating label needs the 32 px row to breathe.
  • Reach for xl / 2xl for marketing-style hero CTAs and full-bleed mobile inputs. Rare in app UI.
  • sm is occasionally right for very dense inspector grids and compact tag rows — only when md would genuinely crowd the layout.
  • 2xs / xs are not general-purpose sizes. They exist for elements rendered inside an Input or similar dense container — a tiny inline indicator, a clear button, a Chip decorating a value. Don't reach for them for standalone controls. A standalone 2xs button is 16 px tall: tap targets fail accessibility, glyphs and text get fragile, and the visual reads as broken rather than intentional.
sm
md
lg
xl
{(['sm', 'md', 'lg', 'xl'] as const).map((size) => (
  <div key={size} className="flex items-center gap-4">
    <span className="w-12 font-mono text-xs text-cladd-fg-soft">
      {size}
    </span>
    <Input size={size} placeholder={`Input size="${size}"`} />
  </div>
))}

Picking a size from this rubric — md for chrome, lg for primary inputs and hero buttons — covers ~90% of cases. When in doubt, md.

Two-size primitives

Checkbox, Radio, Switch, and Slider only ship sm and md — these are inline form primitives that don't need the full ramp. They use their own --spacing-cladd-thumb-* scale:

SizeHeightComponents
sm20 pxCheckbox, Radio, Switch, Slider
md24 px(same)

This is intentionally a third ramp — these primitives are designed to sit inside a root-ramp row at the same token: a size="md" Switch (24 px) inside a size="md" Button or Input row (28 px) has 2 px of breathing room top and bottom. The numbers don't match the root or nested heights at the same token, and that's the point — Switch and friends should read as inline form widgets, not full-width controls.

sm
md
{(['sm', 'md'] as const).map((size) => (
  <div key={size} className="flex items-center gap-4">
    <span className="w-6 font-mono text-xs text-cladd-fg-softer">
      {size}
    </span>
    <Checkbox size={size} defaultChecked />
    <Radio size={size} defaultChecked name={`docs-radio-${size}`} />
    <Switch size={size} defaultChecked />
    <Slider size={size} defaultValue={40} className="w-32" />
  </div>
))}

If you need a third size on these primitives, you're probably reaching for the wrong primitive — a Switch the size of a Button isn't a Switch any more, it's a segmented control.

Pitfalls

  • Don't pick 2xs or xs for standalone controls. They're for nesting inside an Input or similar. A 16 px Button is fragile in isolation; a 20 px Chip standing alone reads as a glitch. Default to md and only go smaller when you've measured the row and it genuinely doesn't fit.
  • Don't mix sizes in a row to "fit" content. If a Chip looks too tall next to its label, the label needs adjustment, not the Chip. Mixing sizes in a row breaks the alignment grid the scale exists to enforce.
  • Don't pass size="md" to a Chip and size="sm" to a Button expecting them to nest correctly. The auto-shrink only works when you pass the same token to both — the Chip already renders 8 px shorter at the same token. Mismatching tokens means doing the math yourself, which defeats the scale.
  • Don't override the heights with h-* or size-* utilities. Components honor their size prop through the underlying h-cladd-* / size-cladd-* utilities — adding a Tailwind h-7 on top fights the scale and breaks the nesting math. If you need a non-standard size, that's a sign the component shouldn't be in this row.
  • Don't put size-* on <svg> children inside Button or Chip. Both components apply a size-matched glyph dimension to direct <svg> children — Button at 12 px (2xs/xs) or 16 px (everything else), Chip ramping 6 → 16 px across the scale. A plain className="size-3.5" on the icon never lands because the parent's selector is more specific; only size-3.5! would win, and the kit's mapping is tuned to be the right call at every step. Pass the right size to the parent.
  • Don't default to lg everywhere. The kit is intentionally dense; lg rows pile up vertical space fast in real apps. Reserve lg for inputs, hero buttons, and marketing-style accents — md is the working default.
  • Don't expect Input at sm to feel right next to a Button at md. Input's floating-label design needs room — at sm the label crowds the value. If you want a 24 px input-like control, use a small SearchField or a styled Chip instead.