Control Heights
The four-rung height scale every control sits on, and why badges deliberately do not.
Overview
Two sizing rules live side by side in the system, and knowing which one a component follows is the whole point of this page.
- A control pins its height. Buttons, inputs, selects, icon buttons, popover triggers, segmented tracks. The height is the design decision — "toolbar controls are 32px" — and the label sits inside it.
- A badge derives its height. Badges, chips, status pills, tags. The label is the content, so height falls out of type token + padding + border, and grows with the type scale.
Mixing the two is what makes a panel header look ragged: a status pill sitting 8px proud of the icon button beside it, a segmented control 4px taller than its neighbouring button.
The scale
Anything that occupies a full row slot beside another control sits on one of four rungs:
| Rung | Token | Height | Typical use |
|---|---|---|---|
xs | --control-h-xs | 28px | Dense table toolbars, inline row actions |
sm | --control-h-sm | 32px | Panel headers, filter bars, secondary chrome |
md | --control-h-md | 36px | Default form controls, page-level actions |
lg | --control-h-lg | 40px | Modal CTAs, prominent actions |
The tokens are declared in packages/config-tailwind/theme.css and consumed as a Tailwind arbitrary property:
// ✓ consume the rung
<div className="h-(--control-h-sm)" />
// ✗ never re-declare the raw pixel — this silently drifts when the scale moves
<div className="h-8" />Which components sit on it
| Component | Rungs it uses |
|---|---|
Button | xs sm md lg |
IconButton | xs sm md lg — square, width comes from the same rung |
Input | sm md |
Select | xs sm md |
Popover trigger | xs sm md lg |
ButtonGroup / SegmentedControl | xs sm md — the track is on the scale, not the buttons inside it |
ButtonGroup is worth calling out: the track carries the height and its buttons fill it with h-full. Sizing the buttons and letting the track add its padding on top is what made a segmented control 4px taller than a lone Button of the same size.
Why controls pin and badges don't
Left to derive, a single size tier resolves five different ways:
All at sm, derived | Height |
|---|---|
| Button, primary (no border) | 34px |
| Button, secondary (border) | 36px |
Input (text-sm) | 30px |
Select (text-md) | 32px |
| Popover trigger | 32px |
A bordered variant comes out 2px taller than a borderless one, and components at the same tier reach for different type tokens. Pinning also lets IconButton be genuinely square, which cannot be derived from a 16px glyph.
Badges go the other way, because the two surfaces run different typography:
| Badge height | sm | md | lg |
|---|---|---|---|
| Brand dashboard | 19px | 24px | 30px |
| Franchisee portal | 20px | 26px | 32px |
The franchisee portal runs larger type; a badge frozen at a brand-dashboard pixel would crowd its own label there. Because every badge-shaped thing runs one shared recipe, badges agree with each other on any given surface without having to agree on a number. Controls hold their rung on both.
This split is not ours alone — Material 3 (chip 32dp / button 40dp), MUI (32 / ~36), Ant (tag ~22 / button 32), Carbon (24 / 40) and Primer (~20 / 32) all size tags independently of buttons.
Count pills
Count pills are the exception in the other direction. They are circles containing a number, so they need a fixed square box rather than either rule:
<span className="size-(--count-pill-size)" /> // 20pxChips do not belong here — they are text-shaped and derive their height.
How it is checked
A vitest helper resolves rendered Tailwind classes against the parsed theme.css and does the arithmetic — no browser. It throws on any class it cannot resolve rather than guessing.
- Controls are asserted against their rung under both typography profiles.
- Badges are asserted to agree with each other and to grow with the type scale, rather than to hit a pixel. Asserting a pixel would reintroduce the coupling the shared recipe removes.
Storybook → Foundations / Control Heights renders all four tiers on a ruled background.
Guidelines
- Do consume
--control-h-*on anything that takes a full slot in a row - Do size a
ButtonGroupby its track, not its buttons - Do let badges, chips, tags and status pills derive their height
- Don't pin a height on a badge-shaped element — it must respond to the type scale
- Don't re-declare a raw
h-8/h-9on a control - Don't reach for a rung to line a badge up with a button; they are not meant to match