Design System Standards
Apple-level design token conventions for cross-platform visual consistency. All visual properties
derive from tokens — no hardcoded values.
The four rules that apply everywhere
- No hardcoded colors in component files — no hex, no
rgb(), no hsl() literals. Consume
tokens via utility classes (bg-primary, text-foreground) or CSS custom properties.
- No arbitrary Tailwind values —
p-[13px], text-[17px], bg-[#ff0000] are all rejected.
Snap to the nearest scale token. If no token fits, question the design first.
- Every interactive element has a visible focus indicator — 2px ring, ≥3:1 contrast, using the
ring-ring token and the focus-visible: prefix (never bare focus:).
- Every animation has a reduced-motion path —
motion-safe: on the web, useReducedMotion()
in JS-driven animation.
// The canonical component line: tokens, focus-visible, guarded motion.
className="bg-primary text-primary-foreground motion-safe:transition-colors motion-safe:duration-150
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
Token naming
All CSS custom properties follow --{category}-{name}:
| Category |
Pattern |
Examples |
| Colors |
Semantic name |
--primary, --secondary, --accent, --success |
| Foreground |
{color}-foreground |
--primary-foreground, --error-foreground |
| Surface |
Surface role |
--background, --card, --popover, --muted |
| Border |
Border role |
--border, --input, --ring |
Colors are declared as space-separated HSL channels with no hsl() wrapper, so Tailwind's
opacity modifier works. Every background token has a contrast-verified -foreground pair.
--primary: 222.2 47.4% 11.2%; /* definition */
background-color: hsl(var(--primary) / 0.5); /* consumption — Tailwind: bg-primary/50 */
Tokens live in :root; dark mode overrides use the .dark class, not
@media (prefers-color-scheme).
Color
- Contrast: ≥4.5:1 for normal text; ≥3:1 for large text (18px+, or 14px+ bold) and UI components.
- Never convey meaning through color alone — semantic colors pair with an icon and a text label.
- Required palettes: Core (
primary/secondary/accent), Neutral (neutral/muted/background/
foreground), Semantic (success/warning/error/info), Surface (card/popover), Border
(border/input/ring).
Typography
Use Tailwind's built-in scale exclusively — no arbitrary font sizes.
text-xs |
text-sm |
text-base |
text-lg |
text-xl |
text-2xl |
text-3xl |
text-4xl |
text-5xl |
| 12px |
14px |
16px (body) |
18px |
20px |
24px |
30px |
36px |
48px |
- Maximum 2 font families per project (sans + mono, or sans + serif).
- Weights: 300, 400, 500, 600, 700 only.
- Line height:
leading-tight (1.25), leading-snug (1.375), leading-normal (1.5),
leading-relaxed (1.625).
Spacing
All spacing is a multiple of 4px, from the Tailwind scale.
| Scale |
Value |
Usage |
0.5-1.5 |
2-6px |
Tight inner padding, icon gaps |
2-4 |
8-16px |
Standard padding, element gaps |
5-8 |
20-32px |
Section padding, card padding |
10-16 |
40-64px |
Major section separation |
20-24 |
80-96px |
Page-level spacing, hero padding |
Padding by atomic level: Atoms p-1-p-3 · Molecules p-2-p-4 · Organisms p-4-p-8 ·
Templates/Pages p-6-p-16.
Motion
Durations: duration-75 (instant feedback) · duration-100 (hover) · duration-150 (default) ·
duration-200 (press, focus) · duration-300 (dropdowns, tooltips) · duration-500 (page
transitions, modals). Ceiling is 500ms — longer feels sluggish.
Easing: ease-out for entering, ease-in for exiting, ease-in-out as default,
cubic-bezier(0.34, 1.56, 0.64, 1) spring for tactile elements (toggles, modals).
Component styling
Multi-variant components use class_variants (Ruby/Phlex) or cva (TypeScript) — never hand-rolled
conditional string concatenation. Five standard axes: size (sm/md/lg/xl), variant
(primary/secondary/outline/ghost/destructive), state (default/hover/active/
disabled/loading), radius (none/sm/md/lg/full), density (compact/default/
comfortable).
Cross-platform
| Platform |
Token source |
Consumption |
| Vite SPA / Next.js |
CSS custom properties |
Tailwind utility classes |
| React Native |
Theme context |
useTheme() hook + StyleSheet |
| Phlex (Rails) |
CSS custom properties |
Tailwind classes + class_variants |
Token names, the 4px spacing base, and type scale ratios are identical on every platform. Touch
targets: 44×44px minimum on mobile, 32×32px on web (WCAG 2.5.8).
Deep guides (read on demand, do not preload)
- Adding a color token, Tailwind wiring, dark mode, contrast verification →
references/defining-tokens.md
cva / class_variants components, focus rings, arbitrary-value escape hatch → references/component-variants.md
- Shared token package, React Native theme, touch targets, parity tests →
references/cross-platform-parity.md
- Framer Motion, Reanimated,
prefers-reduced-motion, animation budgets → references/motion.md
1---2name: std-design-system3description: Design system / token standards — color, typography, spacing, motion tokens; component styling; cross-platform consistency. Use when styling components or defining tokens.4paths:5 - "**/styles/**"6 - "**/components/ui/**"7 - "**/src/theme/**"8 - "**/app/components/**/*.rb"9 - "**/tailwind.config.*"10 - "**/globals.css"11---1213# Design System Standards1415Apple-level design token conventions for cross-platform visual consistency. **All visual properties16derive from tokens — no hardcoded values.**1718## The four rules that apply everywhere19201. **No hardcoded colors** in component files — no hex, no `rgb()`, no `hsl()` literals. Consume21 tokens via utility classes (`bg-primary`, `text-foreground`) or CSS custom properties.222. **No arbitrary Tailwind values** — `p-[13px]`, `text-[17px]`, `bg-[#ff0000]` are all rejected.23 Snap to the nearest scale token. If no token fits, question the design first.243. **Every interactive element has a visible focus indicator** — 2px ring, ≥3:1 contrast, using the25 `ring-ring` token and the `focus-visible:` prefix (never bare `focus:`).264. **Every animation has a reduced-motion path** — `motion-safe:` on the web, `useReducedMotion()`27 in JS-driven animation.2829```tsx30// The canonical component line: tokens, focus-visible, guarded motion.31className="bg-primary text-primary-foreground motion-safe:transition-colors motion-safe:duration-15032 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"33```3435## Token naming3637All CSS custom properties follow `--{category}-{name}`:3839| Category | Pattern | Examples |40|------------|------------------------|-----------------------------------------------------|41| Colors | Semantic name | `--primary`, `--secondary`, `--accent`, `--success` |42| Foreground | `{color}-foreground` | `--primary-foreground`, `--error-foreground` |43| Surface | Surface role | `--background`, `--card`, `--popover`, `--muted` |44| Border | Border role | `--border`, `--input`, `--ring` |4546Colors are declared as **space-separated HSL channels with no `hsl()` wrapper**, so Tailwind's47opacity modifier works. Every background token has a contrast-verified `-foreground` pair.4849```css50--primary: 222.2 47.4% 11.2%; /* definition */51background-color: hsl(var(--primary) / 0.5); /* consumption — Tailwind: bg-primary/50 */52```5354Tokens live in `:root`; dark mode overrides use the **`.dark` class**, not55`@media (prefers-color-scheme)`.5657## Color5859- **Contrast**: ≥4.5:1 for normal text; ≥3:1 for large text (18px+, or 14px+ bold) and UI components.60- **Never convey meaning through color alone** — semantic colors pair with an icon and a text label.61- Required palettes: Core (`primary`/`secondary`/`accent`), Neutral (`neutral`/`muted`/`background`/62 `foreground`), Semantic (`success`/`warning`/`error`/`info`), Surface (`card`/`popover`), Border63 (`border`/`input`/`ring`).6465## Typography6667Use Tailwind's built-in scale exclusively — no arbitrary font sizes.6869| `text-xs` | `text-sm` | `text-base` | `text-lg` | `text-xl` | `text-2xl` | `text-3xl` | `text-4xl` | `text-5xl` |70|-----------|-----------|-------------|-----------|-----------|------------|------------|------------|------------|71| 12px | 14px | 16px (body) | 18px | 20px | 24px | 30px | 36px | 48px |7273- **Maximum 2 font families** per project (sans + mono, or sans + serif).74- **Weights**: 300, 400, 500, 600, 700 only.75- **Line height**: `leading-tight` (1.25), `leading-snug` (1.375), `leading-normal` (1.5),76 `leading-relaxed` (1.625).7778## Spacing7980All spacing is a multiple of **4px**, from the Tailwind scale.8182| Scale | Value | Usage |83|-------------|---------|-----------------------------------------|84| `0.5`-`1.5` | 2-6px | Tight inner padding, icon gaps |85| `2`-`4` | 8-16px | Standard padding, element gaps |86| `5`-`8` | 20-32px | Section padding, card padding |87| `10`-`16` | 40-64px | Major section separation |88| `20`-`24` | 80-96px | Page-level spacing, hero padding |8990Padding by atomic level: Atoms `p-1`-`p-3` · Molecules `p-2`-`p-4` · Organisms `p-4`-`p-8` ·91Templates/Pages `p-6`-`p-16`.9293## Motion9495Durations: `duration-75` (instant feedback) · `duration-100` (hover) · `duration-150` (default) ·96`duration-200` (press, focus) · `duration-300` (dropdowns, tooltips) · `duration-500` (page97transitions, modals). **Ceiling is 500ms** — longer feels sluggish.9899Easing: `ease-out` for entering, `ease-in` for exiting, `ease-in-out` as default,100`cubic-bezier(0.34, 1.56, 0.64, 1)` spring for tactile elements (toggles, modals).101102## Component styling103104Multi-variant components use `class_variants` (Ruby/Phlex) or `cva` (TypeScript) — never hand-rolled105conditional string concatenation. Five standard axes: `size` (`sm`/`md`/`lg`/`xl`), `variant`106(`primary`/`secondary`/`outline`/`ghost`/`destructive`), `state` (`default`/`hover`/`active`/107`disabled`/`loading`), `radius` (`none`/`sm`/`md`/`lg`/`full`), `density` (`compact`/`default`/108`comfortable`).109110## Cross-platform111112| Platform | Token source | Consumption |113|---------------------|------------------------|-------------------------------------|114| Vite SPA / Next.js | CSS custom properties | Tailwind utility classes |115| React Native | Theme context | `useTheme()` hook + `StyleSheet` |116| Phlex (Rails) | CSS custom properties | Tailwind classes + `class_variants` |117118Token **names**, the 4px spacing base, and type scale ratios are identical on every platform. Touch119targets: 44×44px minimum on mobile, 32×32px on web (WCAG 2.5.8).120121## Deep guides (read on demand, do not preload)122123- Adding a color token, Tailwind wiring, dark mode, contrast verification → `references/defining-tokens.md`124- `cva` / `class_variants` components, focus rings, arbitrary-value escape hatch → `references/component-variants.md`125- Shared token package, React Native theme, touch targets, parity tests → `references/cross-platform-parity.md`126- Framer Motion, Reanimated, `prefers-reduced-motion`, animation budgets → `references/motion.md`