crispmeasured from Attio

Foundations

The dimensional scale — spacing, radius, and sizing tokens measured from Attio. The single source of every px in crisp.

Every visual value in crisp is a token, never a literal in a component. Colors flip per theme; dimensions (spacing, radius, sizing) do not — they live in one theme-invariant scale at tokens/scale.ts and are emitted once into :root. A recipe that writes a raw 8px is a bug: it writes c.space["2"] instead.

The scale isn't invented. It was derived by measuring the live Attio sidebar — every gap, padding, and margin — and the values collapse onto a clean 2px-granular, 4px-base ramp. Those measured numbers (1, 2, 4, 6, 8, 10, 12, 16…) are the scale below.

Color

Two tiers: palette (raw hex per theme) → semantic (role → palette ref). Recipes read semantic only, through c.bgBrandSolid, c.fgMuted, and so on — never a hex. Brand and text-on-brand are constant across light/dark; surfaces, borders, and text flip. A theme that drifts fails tsc (the contract is a typed MapLeafNodes).

Brand
bg-brand-solid
bg-brand-solid-pressed
fg-on-brand
Surface
bg-surface
bg-raised
bg-sidebar
Border
border-default
border-subtle
Text
fg-primary
fg-muted
Accent & status
bg-danger
bg-success
bg-warning
bg-orange
bg-cyan
bg-purple
import { contract as c } from "../tokens";

background: c.bgBrandSolid; // the blue, same in both themes
color: c.fgMuted; // secondary text — flips with theme
border-top: `1px solid ${c.borderSubtle}`;

Typography

Attio's UI runs entirely on Inter. Sidebar text measures to just a handful of sizes (11 / 12 / 13 / 14 / 16px — text.lg = 14px is the default body/label) and three weights (400 / 500 / 600, with 500 the UI default). Font family, size, and weight are all tokens; line-height and letter-spacing stay recipe-local for now.

font-family: var(--font-sans) — Inter
text.xs11pxThe quick brown fox shortcut chips
text.sm12pxThe quick brown fox section headers, meta
text.md13pxThe quick brown fox
text.lg14pxThe quick brown fox default body / nav labels
text.xl16pxThe quick brown fox palette input
Aa
weight.regular
400
Aa
weight.medium
500
Aa
weight.semibold
600
font-family: c.font.sans;      // "Inter", system-ui, …
font-size: c.text.lg;          // 14px
font-weight: c.weight.medium;  // 500

Spacing

Gaps, padding, and margins. Keys follow the Tailwind step convention — 0.5 is half a 4px step (2px) — so the numbers stay legible. space.px is the 1px hairline used between rows in a dense list.

space.px1px
space.0.52px
space.14px
space.1.56px
space.28px
space.2.510px
space.312px
space.416px
space.520px
space.624px
import { contract as c } from "../tokens";

// in a recipe — reference the token, never the px
gap: c.space["1.5"];        // 6px
padding: `0 ${c.space["2"]}`; // 0 8px
margin-top: c.space["2.5"];  // 10px
UseTokenpx
Row-to-row in a dense listspace.px1
Tight inner padding (label top/bottom)space["0.5"]2
Icon ↔ label, chevron gapsspace["1"]4
Search-row internal gapspace["1.5"]6
Standard control padding, section spacingspace["2"]8
Header block ↔ first controlspace["2.5"]10
Roomy inline paddingspace["3"]12
Pill / large inline paddingspace["4"]16

Radius

Attio uses three corner radii on its stacked surfaces plus a pill. Nothing else.

radius.sm6px
radius.md8px
radius.lg9px
radius.xl12px
radius.2xl16px
radius.full999px
TokenpxWhere
radius.sm6small chips, badges
radius.md8inputs, search box, nav-adjacent surfaces
radius.lg9rows, workspace tile, list items
radius.full999pills

Sizing

Three recurring dimensions that are neither spacing nor radius — the nav glyph box, the standard row height, and the header height. Component-intrinsic sizes (a badge's 20px, a button's 32px) stay local to their recipe as measured values; only these genuinely-shared dimensions are tokenized.

size.icon16px
size.row28px
size.header48px

Dividers — a zone separator, not a list separator

A divider earns its place only between two functionally different zones of a surface — never between items that belong to the same list (that is what spacing is for). In the sidebar there are exactly two:

  1. between the utility header (workspace switcher + search) and the navigation list, and
  2. between the navigation and the footer (trial / account block).

It renders full-bleed — a 1px border-top in borderSubtle, pulled to the sidebar edges with a negative inline margin equal to the container padding:

.sidebar-divider {
  border: none;
  border-top: 1px solid var(--border-subtle);
  margin: var(--space-2) calc(-1 * var(--space-2)) 0; /* 8px above, full-bleed */
}

Rule of thumb: if you reach for a divider to separate two things in the same list, you actually want spacing. If the two things do different jobs, a divider is right.

Why a scale at all

Two reasons, both learned building the sidebar:

  • Consistency is enforced, not hoped for. When every gap is one of eight tokens, a stray 3px can't creep in and look "almost right." The palette of legal spacings is small and measured.
  • One place to tune. Matching Attio meant nudging real values (the workspace→search gap was 2px; Attio's is 10px = space["2.5"]). With a scale, that's a token reference, not a hunt through recipes for magic numbers.

On this page