Forkful
Design tokens and primitives for the calorie tracker: OKLCH colour, type, 4pt spacing, shape, states.
Colours
Every value is authored in OKLCH so lightness stays perceptually even across the ramp. Colour carries meaning here: coral is brand and carbs, berry is protein, teal is fat. The neutrals are warm (hue 25 to 30) so nothing reads clinical next to food photography.
Brand and macros
Surface
Text
Structural
Status
Colour tokens, semantic roles
| CSS variable | Role | OKLCH | |
|---|---|---|---|
| --coral | brand.primary · macro.carbs | oklch(0.655 0.185 28) | |
| --coral-deep | brand.pressed · text-on-tint | oklch(0.58 0.185 27) | |
| --coral-tint | brand.wash · selected chip | oklch(0.955 0.035 32) | |
| --berry | macro.protein | oklch(0.565 0.205 354) | |
| --berry-deep | macro.protein.text | oklch(0.50 0.205 354) | |
| --teal | macro.fat | oklch(0.70 0.105 195) | |
| --teal-deep | macro.fat.text | oklch(0.52 0.085 197) | |
| --bg | surface.canvas | oklch(0.975 0.006 30) | |
| --surface | surface.card · sheet | oklch(1 0 0) | |
| --surface-2 | surface.inset · input | oklch(0.972 0.005 30) | |
| --ink | text.primary · 14.5:1 | oklch(0.22 0.018 25) | |
| --ink-2 | text.secondary · 7.6:1 | oklch(0.41 0.016 25) | |
| --muted | text.tertiary · 4.7:1 | oklch(0.515 0.014 25) | |
| --line | structural.border | oklch(0.912 0.006 30) | |
| --line-2 | structural.divider | oklch(0.948 0.005 30) | |
| --good | status.on-track | oklch(0.62 0.13 162) | |
| --warn | status.approaching | oklch(0.78 0.145 70) | |
| --over | status.exceeded | oklch(0.60 0.205 25) |
Typography
Albert Sans carries every sentence in the product. Archivo at 125% width and tabular figures carries every number: the calorie ring, macro grams, chart readouts. Splitting the two means a number can grow to 48px without the reading text having to follow it.
Font families
| CSS variable | Stack | Weights | Role |
|---|---|---|---|
| --font-ui | 'Albert Sans', system-ui, sans-serif | 400 · 500 · 600 · 700 | All reading text and controls |
| --font-num | 'Archivo', system-ui, sans-serif | 600 · 700 at wdth 125 | Calories, macros, chart readouts |
Type scale
| CSS variable | Size | Family | Weight | Line height |
|---|---|---|---|---|
| --t-display | 40px | Archivo | 700 | 1.0 |
| --t-metric | 28px | Archivo | 700 | 1.05 |
| --t-h1 | 24px | Albert Sans | 700 | 1.15 |
| --t-h2 | 19px | Albert Sans | 700 | 1.2 |
| --t-title | 16px | Albert Sans | 600 | 1.3 |
| --t-body | 15px | Albert Sans | 400 | 1.45 |
| --t-sm | 13px | Albert Sans | 400 | 1.4 |
| --t-xs | 11px | Albert Sans | 600 | 1.3 |
Spacing and shape
A strict 4pt grid, named by multiple rather than by t-shirt size, so --s6 reads as 24px without a lookup. Radius climbs with the size of the surface: 8px on inline controls, 28px on sheets, full round on pills and the tab bar.
Spacing scale
Radius
Spacing tokens
| CSS variable | Value | Grid step | Usage |
|---|---|---|---|
| --s1 | 4px | 1 | Icon gaps, segmented padding |
| --s2 | 8px | 2 | Chip gaps, label to value |
| --s3 | 12px | 3 | Inline control gaps |
| --s4 | 16px | 4 | Default card padding |
| --s5 | 20px | 5 | Screen gutter, chart card padding |
| --s6 | 24px | 6 | Between blocks in a sheet |
| --s8 | 32px | 8 | Section rhythm |
| --s12 | 48px | 12 | Empty state padding |
| --s16 | 64px | 16 | Scroll clearance above the tab bar |
Radius and elevation
| CSS variable | Value | Applies to |
|---|---|---|
| --r1 | 8px | Focus ring rounding, small tiles |
| --r2 | 12px | Icon buttons inside inputs |
| --r3 | 16px | Primary button, search field, list rows |
| --r4 | 20px | Cards, chart card |
| --r5 | 28px | Bottom sheets |
| --r-full | 9999px | Pills, segmented control, FAB, avatars |
| --shadow-sm | 0 1px 2px rgba(30,12,8,.05) | Resting inline surfaces |
| --shadow-card | 0 1px 3px + 0 6px 16px −10px | Cards |
| --shadow-pop | 0 8px 28px −8px + 0 2px 8px | Sheets, lifted recipe cards |
| --shadow-fab | 0 8px 18px −6px rgba(30,14,10,.26) | Floating action button |
States
Touch targets confirm by shrinking, not by changing colour, so a finger covering the control still gets feedback. Selection is carried by tint plus a 1.5px border plus a darker label, never by tint alone. The focus ring is 2.5px coral at 2px offset on every focusable element.
Primary button
Chip, selectable
Search field
State treatments
| State | Property | Treatment |
|---|---|---|
| default | background / border | --coral / --line at 1.5px |
| hover | background | --coral-deep · 200ms ease-out-quart |
| active, button | transform | scale(0.97) · 120ms ease-out-quint |
| active, icon target | transform | scale(0.85) to scale(0.92) |
| focus-visible | outline | 2.5px solid --coral · offset 2px · radius 6px |
| selected | background / border / color | --coral-tint / --coral / --coral-deep |
| success | background / label | --good · label swaps to "Added" |
| over goal | background / color | --over-tint / --over |
| disabled | opacity / pointer-events | 0.4 · none |
