Developer guide
How to use this design system in a real project. Two files do 90% of the work: tokens.css gives you every design value as a CSS custom property, and shared.css gives you the doc-site layout utilities. Start there.
File structure
Getting started
-
Import
tokens.cssThis gives you every design token as a CSS custom property on:root. It's the only file you need to ship to production — it has no dependencies and no doc-site-specific styles. -
Set the body background and fontUse
--s-canvasfor the page background and--font-bodyfor base font-family. Never use#fff— the warm cream is the brand. -
Add the paper grainCopy the
body::beforegrain layer fromshared.css. It's an SVG turbulence filter inlined as a data URI — no external asset needed. Don't skip it. -
Load the Google FontsBricolage Grotesque (opsz variable, wght 400–700), Onest (wght 400–700), JetBrains Mono (wght 400–600). All three are required. The system looks wrong with system fonts in any of these roles.
-
Copy component styles as neededEach component page has a
<style>block with the component CSS — copy it into your project. Don't importshared.cssinto production (it contains doc-site-only styles like.ds-preview,.ds-code, etc.).
<!-- 1. Fonts --> <link rel="preconnect" href="https://fonts.googleapis.com"> <link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,500;12..96,600;12..96,700&family=Onest:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"> <!-- 2. Tokens --> <link rel="stylesheet" href="path/to/tokens.css"> <!-- 3. Base body styles --> <style> body { background: var(--s-canvas); color: var(--ink); font-family: var(--font-body); font-size: 15px; -webkit-font-smoothing: antialiased; } /* 4. Paper grain — copy from shared.css */ body::before { content: ""; position: fixed; inset: 0; pointer-events: none; z-index: var(--z-grain); opacity: .3; background-image: url("data:image/svg+xml;utf8,…"); mix-blend-mode: multiply; } </style>
CSS architecture
Settld uses a BEM-lite naming convention: block-element (no double underscore, no double dash modifier). Modifiers use a single dash: btn-primary, btn-sm. State is a separate class: active, disabled, loading.
.btn-primary
.btn-sm
.btn-loading
.person-chip
.person-chip.selected
.composer-row
.ledger-row
.btn--primary
.button-is-loading
style="color: #E76F51"
style="font-size: 15px"
.personChip (camelCase)
#settle-card (IDs for style)
Token-first values. Every color, spacing, radius, shadow, duration, and easing must reference a token, not a raw value. If a token doesn't exist for what you need, add it to tokens.json first, then regenerate tokens.css.
/* Token-first — correct */ .card { background: var(--s-paper); border-radius: var(--r-lg); box-shadow: var(--shadow-lg); padding: var(--sp-6); /* 24px */ border: 1px solid var(--ink-line); } /* Raw values — never do this */ .card { background: #FBF8F0; /* ← magic number */ border-radius: 24px; /* ← untracked */ padding: 24px; /* ← not on the grid */ }
Dark mode
Dark mode is a data-theme="dark" attribute toggle on the <html> or root element. tokens.css includes a [data-theme="dark"] override block that remaps all surface and ink tokens. Component CSS uses these tokens — so dark mode is free if you write token-first CSS.
/* Toggle dark mode */ document.documentElement.dataset.theme = document.documentElement.dataset.theme === 'dark' ? '' : 'dark'; /* Or: respect OS preference */ @media (prefers-color-scheme: dark) { :root { /* same overrides as [data-theme="dark"] */ } }
Responsive approach
Mobile-first. Write base styles for mobile (375px), then add desktop overrides inside breakpoint queries.
| Token | Value | Use for |
|---|---|---|
--bp-sm (520px) | 520px | Small phone to large phone |
--bp-md (720px) | 720px | Tablet portrait, large phone |
--bp-lg (900px) | 900px | Tablet landscape |
--bp-xl (1240px) | 1240px | Desktop |
/* Mobile-first: base is mobile */ .composer { max-width: 100%; border-radius: var(--r-lg); /* 24px — slightly tighter on small screens */ } @media (min-width: 720px) { .composer { max-width: 420px; border-radius: var(--r-xl); /* 28px on larger screens */ } }
Performance: transition discipline
Never use transition: all. Always enumerate the properties you're transitioning. transition: all causes the browser to check every property for changes on every frame — including layout-triggering properties like width and height.
The properties safe to animate on the GPU (no layout reflow): transform, opacity, background-color, color, box-shadow, border-color, outline. Avoid animating width, height, padding, margin, top, left.
/* Correct — explicit properties */ .btn { transition: background var(--d-fast) var(--ease-out), color var(--d-fast) var(--ease-out), transform var(--d-fast) var(--ease-out), box-shadow var(--d-fast) var(--ease-out); } /* Wrong — catches everything, including layout properties */ .btn { transition: all 140ms ease; }
Hover transform guard
All hover transforms (translateY, rotateX, scale on hover — not press) must be wrapped in @media (hover: hover) and (pointer: fine). This prevents sticky-hover on touchscreens, where the element stays in its hover state after a tap.
/* Press state — always active */ .card:active { transform: scale(0.97); } /* Hover state — only for pointer devices */ @media (hover: hover) and (pointer: fine) { .card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); } }
Accessibility checklist
Before shipping any new component or screen, run through this list. It's not exhaustive — see the full Accessibility guidelines page for detail.
- Every interactive element is reachable by keyboard (Tab, Shift+Tab)
- Focus ring is visible on all interactive elements (
:focus-visiblerule applied) - All buttons have text labels (or
aria-labelfor icon-only) - Color is not the sole carrier of information (e.g., error has both coral color and error text)
- All animations respect
prefers-reduced-motion - Touch targets are at least 44×44px
- Modals and sheets trap focus and return it on close
- Images and decorative elements have
aria-hidden="true"oralt="" - Dynamic content updates use
role="status"orrole="alert"as appropriate - WCAG AA contrast met for all body text and interactive elements
Token quick-reference
| Category | Key tokens | Notes |
|---|---|---|
| Surfaces | --s-canvas --s-warm --s-deep --s-paper |
Canvas = page bg. Paper = cards, modals. Warm/deep = inset surfaces, segmented controls. |
| Ink | --ink --ink-body --ink-mute --ink-line |
Primary → body → muted → hairline. Use in order of visual hierarchy. |
| Accent | --teal --teal-deep --teal-pale --coral --coral-pale --butter |
Teal = settle/success. Coral = error/owe. Butter = celebration. Deep/pale = hover/bg. |
| Semantic | --success --warning --error --info (+ -bg variants) |
Use semantic tokens for status states, not raw accent tokens. |
| Glass | --glass-bg --glass-border |
Frosted glass surfaces only. Requires backdrop-filter. Never on white or same-surface. |
| Typography | --font-display --font-body --font-mono |
Display = Bricolage. Body = Onest. Mono = JetBrains. Never substitute. |
| Radius | --r-xs --r-sm --r-md --r-lg --r-xl --r-pill |
xs=6 sm=10 md=14 lg=24 xl=28 pill=999px |
| Shadow | --shadow-sm --shadow-md --shadow-lg --shadow-float |
All warm-tinted. sm=hairline, md=hover/menu, lg=cards, float=modals/settle card. |
| Motion | --ease-out --ease-spring --ease-ios --d-fast --d-med --d-slow |
fast=140ms (buttons), med=240ms (cards), slow=420ms (mode switches). |
| Z-index | --z-sticky --z-nav --z-modal --z-toast --z-grain |
Grain (1000) is always on top — it's the paper texture. Never let a component exceed it. |