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

settld_design_system/
tokens.json ← source of truth. Edit this.
tokens.css ← generated CSS custom properties. Import this.
shared.css ← doc site utilities only. Don't ship to product.
wordmark.css ← wordmark variants and colorways.
index.html ← design system home
foundations/
color.html
typography.html
spacing-grid.html
motion.html
components/
button.html ← reference template for sidebar HTML
… (one file per component)
patterns/
expense-form.html
settle-flow.html
navigation.html
lists-feeds.html
guidelines/
principles.html
accessibility.html
content.html
dev-guide.html

Getting started

  1. Import tokens.css
    This 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.
  2. Set the body background and font
    Use --s-canvas for the page background and --font-body for base font-family. Never use #fff — the warm cream is the brand.
  3. Add the paper grain
    Copy the body::before grain layer from shared.css. It's an SVG turbulence filter inlined as a data URI — no external asset needed. Don't skip it.
  4. Load the Google Fonts
    Bricolage 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.
  5. Copy component styles as needed
    Each component page has a <style> block with the component CSS — copy it into your project. Don't import shared.css into 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.

Do — BEM-lite
.btn
.btn-primary
.btn-sm
.btn-loading
.person-chip
.person-chip.selected
.composer-row
.ledger-row
Don't — BEM strict or inline
.btn__label
.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"] */ }
}
Dark mode status The dark mode token overrides are defined, but the visual design hasn't been fully reviewed on a dark surface. The Moments section on the landing is the only production-tested dark surface. Before shipping full dark mode, do a pass on: glass card legibility, coral on dark backgrounds (contrast drops), and the paper grain (opacity may need to increase).

Responsive approach

Mobile-first. Write base styles for mobile (375px), then add desktop overrides inside breakpoint queries.

TokenValueUse for
--bp-sm (520px)520pxSmall phone to large phone
--bp-md (720px)720pxTablet portrait, large phone
--bp-lg (900px)900pxTablet landscape
--bp-xl (1240px)1240pxDesktop
/* 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.

Token quick-reference

CategoryKey tokensNotes
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.