Sizing
Sizing tokens define the physical bounds of UI: widths, heights, and min/max constraints used to build interfaces that are consistent, accessible, and natively responsive.
This system is built on two explicit layers:
- Core Tokens — intent-free primitives and the responsive engine
- Semantic Tokens — sizing contracts consumed by UI code
Rule: Core tokens are never referenced in components.
Core Tokens
Core sizing tokens are intent-free primitives and the single source of truth for responsiveness.
They exist to:
- centralize fluid logic (all
clamp()formulas live here) - keep semantic tokens as aliases (stable names, theme-tunable engine)
- make the system predictable (no ad-hoc coefficients in semantics)
Core tokens are never consumed by components. Components consume semantic tokens only.
Output Guidance (Web)
The following is specific to CSS/web output and does not affect the semantic contract.
- A query container rule for layout surfaces:
.tt-container {
container-type: inline-size;
}
- A robust fallback strategy in build output:
- emit a viewport-safe fallback first
- override with container units when supported
@supports (width: 1cqi) {
/* container-based overrides */
}
- The core token set below (ramps + primitives).
Core groups
1) Fluid ramps (required)
Ramps are the engine. They are bounded ranges expressed with clamp(min, preferred, max).
core.sizing.ramp.ui.1..8— small→medium objects such as icons and identitycore.sizing.ramp.layout.1..6— medium→large structural bounds such as surfaces
Rule: semantic fluid tokens (
icon.*,identity.*,surface.maxWidth) should map to ramp steps, not define new formulas.
2) Primitives (required)
core.sizing.relative.em = 1emcore.sizing.relative.rem = 1remcore.sizing.behavior.auto = autocore.sizing.behavior.full = 100%core.sizing.behavior.fit = fit-contentcore.sizing.behavior.min = min-contentcore.sizing.behavior.max = max-contentcore.sizing.viewport.height.full = 100dvhcore.sizing.viewport.width.full = 100dvw
3) Ergonomic hit primitives (required)
core.sizing.hit.fine— the single ergonomic floor for fine pointer (mouse, trackpad); may be fluid viaclamp(floor, preferred, max)whereflooris a fixed px minimumcore.sizing.hit.coarse— the single fixed px floor for coarse pointer (touch); never fluid
hit is one value per pointer profile, not a scale — the theme's single ergonomic minimum for an interactive target (ADR-020). Coarse is always fixed px — reliable ergonomic guarantees for touch. Fine may use clamp(floor, preferred, max) where floor is a fixed px ergonomic minimum, so the rem preferred respects user font-size while accessibility is always guaranteed. The build output emits the fine value as the baseline and the coarse value inside @media (any-pointer: coarse) automatically.
Example
const coreSizing = {
sizing: {
ramp: {
ui: {
1: 'clamp(12px, calc(0.6cqi + 10px), 16px)',
2: 'clamp(14px, calc(0.8cqi + 11px), 20px)',
3: 'clamp(16px, calc(1.0cqi + 12px), 24px)',
4: 'clamp(20px, calc(1.2cqi + 14px), 32px)',
5: 'clamp(24px, calc(1.5cqi + 16px), 40px)',
6: 'clamp(32px, calc(1.8cqi + 20px), 56px)',
7: 'clamp(40px, calc(2.2cqi + 24px), 72px)',
8: 'clamp(48px, calc(2.6cqi + 28px), 96px)',
},
layout: {
1: 'clamp(320px, 40cqi, 480px)',
2: 'clamp(384px, 50cqi, 640px)',
3: 'clamp(480px, 60cqi, 800px)',
4: 'clamp(560px, 70cqi, 960px)',
5: 'clamp(640px, 80cqi, 1120px)',
6: 'clamp(768px, 90cqi, 1280px)',
},
},
relative: {
em: '1em',
rem: '1rem',
},
behavior: {
auto: 'auto',
full: '100%',
fit: 'fit-content',
min: 'min-content',
max: 'max-content',
},
viewport: {
height: {
full: '100dvh',
},
width: {
full: '100dvw',
},
},
hit: {
// A single ergonomic floor per pointer profile (ADR-020).
// Fine: clamp(floor, preferred, max) — floor is fixed px; preferred scales
// with rem (not cqi), so a control's height never grows with the window.
// Tuned desktop-first at 32px (GitHub/Linear ~32, Stripe ~36).
fine: 'clamp(32px, 2rem, 36px)',
// Coarse: always fixed px — touch ergonomics require a predictable,
// reliable target. 48px sits above the 44px Apple HIG floor.
coarse: '48px',
},
},
};
Expected consumption pattern: semantic tokens reference core tokens by alias.
Example: icon.md → core.sizing.ramp.ui.3, surface.maxWidth → core.sizing.ramp.layout.5.
Semantic Tokens
Sizing semantics are anchored in geometry and ergonomics, not UX categories. This avoids ambiguity and prevents token-per-component drift.
Token structure
{family}.{stepOrProperty}
family: what kind of physical sizing contract this isstepOrProperty: the specific step or property inside that family
Families
{family} | Description |
|---|---|
hit | the single minimum interactive target floor (ergonomic contract). One value per pointer profile; adapts to input capability. Fine may be fluid with clamp() (floor must be fixed px). Coarse is always fixed px. |
icon | visual glyph sizing only. May be fluid via core ramp. |
identity | visual identity object sizing (profile / brand / entity). May be fluid via core ramp. |
measure | readability measure (line-length contract, character-based). |
surface | structural bounds for UI surfaces (constraints, not components). |
| viewport | viewport primitives for full-height and full-width layouts. |
Canonical shapes
hiticon.{sm|md|lg}identity.{sm|md|lg|xl}measure.readingsurface.maxWidthviewport.height.fullviewport.width.full
Semantic Tokens Summary Table
| token | use when you are building… | contract (must be true) | default value |
|---|---|---|---|
hit | any interactive target (buttons, inputs, toggles, list rows) | minimum interactive area; not visual size; must not shrink; enforce via min-* | theme-defined ergonomic floor |
icon.sm | small glyphs / dense UI | visual only; bounded range via core ramp | core.sizing.ramp.ui.2 |
icon.md | standard icons | visual only; bounded range via core ramp | core.sizing.ramp.ui.3 |
icon.lg | prominent icons | visual only; bounded range via core ramp | core.sizing.ramp.ui.4 |
identity.sm | compact identity objects | visual only; bounded range via core ramp | core.sizing.ramp.ui.5 |
identity.md | standard identity objects | visual only; bounded range via core ramp | core.sizing.ramp.ui.6 |
identity.lg | prominent identity objects | visual only; bounded range via core ramp | core.sizing.ramp.ui.7 |
identity.xl | hero identity / brand objects | visual only; bounded range via core ramp | core.sizing.ramp.ui.8 |
measure.reading | long-form text containers | single bounded readability contract | clamp(45ch, 60ch, 75ch) |
surface.maxWidth | cards, panels, dialogs, surface shells | bounded structural max width; container-first | core.sizing.ramp.layout.5 |
viewport.height.full | full-height layouts | must use dynamic viewport units; use intentionally for full-height layouts | core.sizing.viewport.height.full |
viewport.width.full | full-width layouts | must use dynamic viewport units; use intentionally for full-width layouts | core.sizing.viewport.width.full |
Accessibility note: WCAG 2.2 Target Size (Minimum) defines a lower baseline of 24×24 CSS px, with exceptions. ttoss recommends stronger ergonomic baselines, especially for coarse pointer environments, while allowing themes to tune values based on product needs.
Hit target adaptation
hit defines an ergonomic contract, not a fixed pixel value.
Implementations should adapt hit targets based on input capability:
- fine pointer (
mouse,trackpad) → more compact targets - coarse pointer (
touch) → larger targets
The build output handles this automatically. Fine values are emitted as the baseline; coarse values are injected based on input capability detection.
Output Guidance (Web)
In CSS output, the fine value is the baseline and the coarse value is injected inside @media (any-pointer: coarse):
:root {
--tt-sizing-hit: clamp(
32px,
2rem,
36px
); /* fine baseline — fluid via rem, ergonomic floor guaranteed */
}
@media (any-pointer: coarse) {
:root {
--tt-sizing-hit: 48px; /* touch override — fixed px, always reliable */
}
}
The semantic token remains stable (
hit). The runtime adapts the value.
Control geometry adapts to user font (rem) — never to the container
(cqi), which is reserved for layout spacing/sizing — so a control's height
never grows just because the window is wider. See ADR-020.
Rules of Engagement (non-negotiable)
- Hit vs visual: never use
icon.*oridentity.*as hit targets; always enforcehitviamin-width/min-height. - Reading vs surface: use
measure.readingfor long text; usesurface.maxWidthfor structural wrappers. - No responsive logic in components: responsiveness lives in Core (ramps + container units), not in component code.
- Dynamic dimensions: avoid
100vhand100vw; useviewport.height.fullandviewport.width.full.
Theming
Themes may tune:
- the core ramps (
core.sizing.ramp.ui.*,core.sizing.ramp.layout.*) surface.maxWidthmapping to a different layout ramp stepmeasure.readingin rare cases, validated with real contentcore.sizing.hit.fineto tune the ergonomic floor for mouse; may useclamp(floor, preferred, max)whereflooris fixed pxcore.sizing.hit.coarseto adjust the ergonomic floor for touch; always fixed px
Semantic token names never change across themes.
Validation
Errors (validation must fail when)
-
hit.coarseresolves to a fluid or intrinsic value, includingclamp(...),cqi,%,auto, or content-sizing keywords -
hit.fineusesclamp()without a fixed px floor (the minimum bound must be a literalNpxvalue, not a variable or formula) -
the coarse-pointer hit floor is smaller than the fine-pointer floor:
coarse < fine(compare coarse fixed px to fine clamp floor)
-
measure.readingis not a bounded character-based measure -
generated output does not emit fine-pointer hit values as the baseline and coarse-pointer hit values inside
@media (any-pointer: coarse) -
generated output does not emit a viewport-safe fallback before container-based overrides
-
generated output does not gate container-based overrides behind
@supports (width: 1cqi) -
generated output emits
viewport.height.fullasvhinstead of dynamic viewport units -
generated output emits
viewport.width.fullasvwinstead of dynamic viewport units
Warning (validation should warn when)
-
any
icon.*token resolves outsidecore.sizing.ramp.ui.* -
any
identity.*token resolves outsidecore.sizing.ramp.ui.* -
surface.maxWidthresolves outsidecore.sizing.ramp.layout.* -
viewport.height.fulldoes not resolve tocore.sizing.viewport.height.full -
viewport.width.fulldoes not resolve tocore.sizing.viewport.width.full -
a resolved
hitvalue (fine or coarse) is below24px -
adjacent
icon.*tokens resolve to the same effective value -
adjacent
identity.*tokens resolve to the same effective value -
the fine-pointer and coarse-pointer hit floors resolve to the same value
-
measure.readingandsurface.maxWidthresolve to the same effective value
Summary
- Core sizing defines primitives plus the responsive ramps
- Semantic sizing defines a small set of stable geometry contracts
hitis ergonomic, not visualicon.*andidentity.*are visual, not interactivemeasure.readingandsurface.maxWidthsolve different problems- Responsiveness lives in the core engine, not in components
- The system stays small, predictable, and scalable