ADR-005 — size vs space Primitives
Date: 2026-06-11
Status: accepted
Context
The primitive layer has two numeric scales that both affect layout: space and size. They share the same underlying grid unit (multiples of 4px) and their values overlap. Without a clear rule, contributors building components face an ambiguous choice: does a button’s height come from space or size?
A secondary issue: the original size primitive keys used T-shirt names (xs, sm, md, lg, xl, xxl) while space uses a numeric scale (050, 100, 200…). This inconsistency made the two scales feel unrelated even though they share the same base unit.
Decision
space.* is for empty space — the absence of content.
It answers: “how much distance goes between or around things?”
Used for: gap, padding, margin — in component CSS Modules via semantic aliases (space.inset.*, space.stack.*, space.inline.*).
size.* is for filled elements — the presence of content.
It answers: “how big is this element?”
Used for: width, height, min-width, min-height of component elements like icons, avatars, badges, and control heights.
These are semantically distinct even when their pixel values coincide. size.300 = 24px and space.300 = 24px are the same raw value but different intents — one describes an avatar’s diameter, the other describes a gap between siblings. The token name carries that intent across the entire system.
Numeric keys on size match the space scale.
size primitive keys are now numeric and mirror the corresponding space keys (size.200 = 16, size.300 = 24, etc.). The size scale only covers the values meaningful for component dimensions — it does not include 000 (0) or 050 (4px) because no filled component has a dimension of 0 or 4px.
size key |
value | matching space key |
|---|---|---|
size.200 |
16px | space.200 |
size.300 |
24px | space.300 |
size.400 |
32px | space.400 |
size.500 |
40px | space.500 |
size.600 |
48px | space.600 |
size.800 |
64px | space.800 |
T-shirt names belong at the semantic layer, not the primitive layer.
When a component needs size variants, a semantic alias is defined in the theme layer that maps a named size to a size.* primitive:
// theme/light.json (future — added when the component is built)"icon": { "size": { "sm": { "$type": "number", "$value": "{size.200}" }, "md": { "$type": "number", "$value": "{size.300}" }, "lg": { "$type": "number", "$value": "{size.400}" } }},"avatar": { "size": { "sm": { "$type": "number", "$value": "{size.300}" }, "md": { "$type": "number", "$value": "{size.400}" }, "lg": { "$type": "number", "$value": "{size.600}" } }}icon.size.sm and avatar.size.sm are both “small” but resolve to different primitives — because smallness is relative to the component type. The semantic layer is where that judgment is encoded.
Consequences
- Component CSS Modules reference semantic size aliases, never raw
size.*primitives directly.var(--ds-icon-size-md)is correct;var(--ds-size-300)in a component is a smell that the semantic alias is missing. - Cross-use is a bug. Using
space.*for a component’s width or height, or usingsize.*for a gap or padding, misrepresents intent. Linters or ADR-based code review should catch this. - New fixed-dimension needs add a
size.*key only if the value doesn’t already exist. If a new component needs a 56px dimension, addsize.700— don’t repurpose an existing key. - Semantic size aliases are added component-by-component as components are built in Phase 1.7, not pre-emptively.