This system exists to serve one builder across many projects. It eliminates repeated design decisions so that attention goes toward the unique utility each project provides. Familiarity across projects is a feature — not visual sameness, but a shared logic that makes every project feel known.
It believes in density with clarity, within platform constraints. On platforms with weak layout opinions (web), interfaces should reward attention and make information available rather than hidden. Visual hierarchy does the work — type weight, spatial grouping, restrained color. On platforms with strong opinions (macOS), the platform's spatial and information model takes precedence. The system's density values express themselves only within the room the platform leaves.
It is native to each platform it touches. Platform idiom always wins. The system provides shared values and shared tokens, not a shared skin.
It prioritizes the user of the thing over the builder of the thing. Polish over speed. Accessibility over novelty. Smaller payloads over simpler builds. Complexity lives in the build pipeline, never on the screen.
It distrusts decoration and values economy. If something is visible, it is useful.
The constitution is organized into two parallel structures: Rules (how things are built) and Tokens (the concrete values used). Both are tiered — universal foundations first, then platform-specific layers, then complexity-specific refinements.
All design tokens are authored in the W3C Design Token Community Group format (JSON). This is the single source of truth for every value in the system.
Build tooling: Style Dictionary compiles the token source files into platform-specific outputs.
Output targets:
Schema rules:
space
text
color
font
leading
text.base
text.14
color.accent
color.blue
Example (W3C DTCG format):
{ "space": { "1": { "$value": "4px", "$type": "dimension" }, "2": { "$value": "8px", "$type": "dimension" }, "4": { "$value": "16px", "$type": "dimension" } }, "text": { "base": { "$value": "14px", "$type": "dimension" }, "md": { "$value": "18px", "$type": "dimension" }, "lg": { "$value": "22px", "$type": "dimension" } }, "font": { "serif": { "$value": "Charter, 'Bitstream Charter', 'Noto Serif', 'Book Antiqua', Georgia, serif", "$type": "fontFamily" }, "sans": { "$value": "-apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Helvetica, Arial, sans-serif", "$type": "fontFamily" } } }
These rules and tokens apply to every project on every platform regardless of complexity.
Rules:
Tokens:
space.1
space.2
space.3
space.4
space.5
space.6
space.8
space.10
space.12
space.16
Tokens — Type Scale:
text.xs
text.sm
text.md
text.lg
text.xl
text.2xl
Tokens — Line Height:
leading.tight
leading.normal
leading.relaxed
Tokens — Font Stacks (web):
font.serif
font.sans
font.mono
Typographic Role Assignments:
Philosophy: Color is functional, not decorative. The system is grayscale-dominant. Color enters the interface to communicate meaning (status, interactivity, focus) or to provide per-project identity through a single accent hue. The system never uses color for ornamentation.
Construction method: Neutrals are hand-picked. Functional color scales (accent, success, warning, error) are generated in OKLCH color space from seed hues, ensuring consistent perceived lightness across hues. This guarantees that a success green and error red at the same scale step have equivalent contrast against any background.
Contrast target: WCAG AA minimum (4.5:1 for normal text, 3:1 for large text and UI components). Body text on primary backgrounds should meet AAA (7:1) where possible — the grayscale-dominant palette makes this achievable by default.
Tokens — Neutral Scale (hand-picked):
color.neutral.950
color.neutral.900
color.neutral.700
color.neutral.500
color.neutral.300
color.neutral.200
color.neutral.100
color.neutral.50
color.neutral.0
Dark mode strategy: Systematic transformation. The neutral scale inverts — neutral.950 in light mode maps to neutral.950 in dark mode, but the underlying value flips. Semantic tokens reference the scale by role, not by value, so dark mode is an output target from the same semantic layer. Accent and functional colors adjust lightness to maintain contrast ratios against the inverted backgrounds. Dark mode is a build output, not a hand-maintained parallel palette.
neutral.950
Accent color — per-project:
Each project defines a single accent hue (0–360 on the OKLCH hue wheel). The system generates a full accent scale from that hue:
color.accent.base
color.accent.hover
color.accent.muted
color.accent.subtle
color.accent.text
A project's token source file specifies only the hue. Style Dictionary generates the full scale at build time, adjusting lightness per step to meet contrast requirements against both light and dark mode backgrounds.
Example project token override:
{ "color": { "accent": { "hue": { "$value": "250", "$type": "number" } } } }
The build pipeline reads this hue and generates the accent scale using predefined OKLCH lightness and chroma recipes.
Functional colors (semantic status):
These use fixed hues, generated via the same OKLCH method as accent colors. They are system-wide and do not vary per project.
color.success.*
color.warning.*
color.error.*
color.info.*
Each functional color follows the same scale shape as accent (base, hover, muted, subtle, text).
Semantic surface and text tokens:
These are the tokens that components and layouts actually consume. They alias into the neutral and accent scales.
color.surface.primary
neutral.0
color.surface.secondary
neutral.50
color.surface.tertiary
neutral.100
color.text.primary
color.text.secondary
neutral.700
color.text.tertiary
neutral.500
color.border.default
neutral.200
color.border.emphasis
neutral.300
Categorical colors — per-project:
Some projects need to color-code domain-specific data categories — plant types, task categories, tag groups, area types. These are not functional status colors (success/warning/error) and should not reuse them. Categorical colors are informational, not interactive: they label and differentiate, but carry no hover or muted states.
Each project declares named categories with hues in its project token file. The build system generates three variants per category using the same OKLCH method as functional colors:
base
oklch(0.55 0.18 H)
oklch(0.65 0.17 H)
subtle
oklch(0.95 0.025 H)
oklch(0.18 0.03 H)
surface.primary
oklch(0.45 0.16 H)
oklch(0.75 0.15 H)
CSS custom property naming: --color-cat-{name}-{variant} (e.g. --color-cat-vegetable-base).
--color-cat-{name}-{variant}
--color-cat-vegetable-base
Example project token file with categories:
{ "project": { "accent": { "hue": { "$value": 55, "$type": "number" } }, "categories": { "vegetable": { "hue": { "$value": 145, "$type": "number" } }, "herb": { "hue": { "$value": 290, "$type": "number" } } } } }
When to use categorical colors vs. functional colors:
success
warning
error
info
Application pattern: Use CSS data attributes to map categories to colors. This keeps color logic in CSS and out of component code:
.type-badge[data-type="vegetable"] { background-color: var(--color-cat-vegetable-subtle); color: var(--color-cat-vegetable-text); }
color.error
Philosophy: States are communicated through the tokens the system already provides — neutral scale shifts, accent color, and opacity. No new colors are introduced for states. This ensures states work automatically with any per-project accent hue and keeps the token surface small.
Canonical states:
accent.hover
accent.base
accent.subtle
opacity: 0.4
not-allowed
Active vs. Selected: Active is momentary — the element is being pressed right now. Selected is persistent — the element has been chosen and remains in that state. A table row can be hovered, focused, and selected simultaneously. A button can be active (pressed) but is never selected. A checkbox or toggle is selected when checked. These two states are never interchangeable.
Transition rules:
150ms ease-out
Focus visibility:
:focus-visible
2px solid
2px
Minimum interactive target sizes:
Philosophy: Motion is functional. It exists to preserve spatial continuity and soften jarring transitions. It does not exist to delight, entertain, or add personality. If removing an animation would not reduce comprehension, the animation should not exist.
Duration scale:
duration.fast
duration.normal
duration.slow
Nothing in the system exceeds 250ms. If something feels like it needs a longer animation, it should not animate.
Easing: One curve for the entire system: ease-out. Fast start, gentle landing. No ease-in (sluggish), no linear (mechanical), no ease-in-out (floaty). Continuous animations (loading spinners) use linear as the sole exception.
ease-out
linear
What transitions:
What does not animate:
Reduced motion: When prefers-reduced-motion: reduce is active, all durations become 0ms. Every transition becomes instant. This is a hard rule — not a reduction, a removal. Loading spinners are the sole exception; they may continue but should use a simple opacity pulse instead of rotation.
prefers-reduced-motion: reduce
0ms
Philosophy: The system does not use a material/elevation metaphor. Surfaces are flat. Hierarchy is created by background color shifts and borders, not by shadows suggesting physical depth. Shadows exist only to indicate that an element is genuinely floating — detached from the page flow.
Surface hierarchy: Surfaces are nested using the neutral background scale. Each nesting level shifts one step deeper on the scale. Borders separate elements at the same level. Background shifts separate elements at different levels.
Three levels is the maximum nesting depth. If a design requires a fourth level, it should be restructured.
Borders:
border.default
border.emphasis
0
Shadows — floating elements only:
Shadows are used exclusively for elements that are detached from the normal document flow: dropdowns, tooltips, popovers, command palettes, and toasts. There is one shadow value in the system.
shadow.float
0 2px 8px rgba(0, 0, 0, 0.12)
In dark mode: 0 2px 8px rgba(0, 0, 0, 0.4).
0 2px 8px rgba(0, 0, 0, 0.4)
The shadow is small, tight, and sharp. It communicates "this is floating" without decorative diffusion. There is no shadow scale — one value covers all floating cases.
Z-index conventions:
z.base
z.sticky
z.dropdown
z.overlay
z.toast
No arbitrary z-index values. All layered elements use a token from this scale.
Backdrop overlay:
box-shadow
Philosophy: Icons are secondary to text. A label is always preferred over an icon where space permits. Icons are used when space is constrained (toolbars, dense table actions) or when a concept is universally understood without a label (close, search, settings, external link, expand/collapse). When an icon's meaning is ambiguous, it must be paired with a text label.
Icon set: Lucide (web). Open source, outline style, consistent stroke weight, available as individual SVGs for tree-shaking. The outline style matches the system's line-and-type character. On macOS/SwiftUI, use SF Symbols — the platform-native set always wins.
Size grid:
icon.sm
icon.md
Two sizes only. Both align to the 4px spatial grid. No other icon sizes are used.
Stroke weight: 1.5px at both sizes. This optically matches the 1px borders used throughout the system. Lucide's default is 2px at 24px; the reduced weight at smaller sizes prevents icons from feeling heavier than surrounding text and line work.
Color: Icons inherit the text color of their context. An icon next to text.primary content is text.primary. An icon in a text.secondary label is text.secondary. Icons never introduce their own color unless they are communicating a functional status (using color.success, color.error, etc.).
text.primary
text.secondary
color.success
These extend the universal foundation with rules and tokens specific to each platform.
Cascade layers. The system uses @layer to establish deterministic specificity ordering. Three system layers, in order:
@layer
@layer reset, base, components;
Token custom properties are declared on :root outside any layer — they are always available and never participate in specificity conflicts. Project-specific CSS is written outside all layers (or in a project-defined layer above components), so it always wins over system styles without !important or specificity tricks.
:root
components
!important
reset
Metal-tier projects import only tokens.css (the :root custom properties). No layers are used.
tokens.css
No preprocessors. The system CSS is plain CSS. No Sass, Less, or PostCSS required. CSS custom properties handle theming and dynamic values. Nesting uses native CSS nesting where supported. Projects are free to use preprocessors for their own code, but the design system does not require them.
No utility classes. The system does not provide or use utility class methodologies (Tailwind, etc.). Styling is applied through semantic classes (component names) and direct CSS using token custom properties. This keeps the HTML readable and the CSS inspectable.
Philosophy. The system is designed for pointer-driven viewports. Dense, information-rich layouts are the primary target. Small-screen support is a graceful adaptation, not a co-equal design target. Layouts are fluid on both sides of the breakpoint — the system uses relative units, flexible grids, and constrained measures so content adapts smoothly at any width.
Single structural breakpoint:
breakpoint.narrow
Below 640px, structural layout changes occur: multi-column grids collapse, sidebars become hidden or top-stacked, navigation reorganizes. Above 640px, layouts are fluid and expand to fill available space.
This is a min-width breakpoint — the narrow layout is the default, the wide layout is the enhancement. Despite not being a mobile-first system philosophically, the CSS is structured mobile-first technically because min-width produces simpler overrides:
min-width
/* Default: narrow layout */ .grid { grid-template-columns: 1fr; } /* Wide layout */ @media (min-width: 640px) { .grid { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); } }
Fluid behavior within each range:
auto-fill
auto-fit
minmax()
max-width
ch
max-width: 100%
What the breakpoint changes (structural shifts):
What the breakpoint does not change:
Additional breakpoints. Projects may define project-specific breakpoints if needed. The system provides one; it does not forbid others. A project-specific breakpoint should be documented in the project's CLAUDE.md. The system will not provide named tokens for project-specific breakpoints.
Plain CSS classes, not web components. Components are semantic HTML elements styled with system classes. No shadow DOM, no custom elements, no JavaScript framework dependency at the system level. Classes are directly inspectable, predictable, and easy for Claude Code to generate.
Pattern:
<!-- Semantic HTML + system class + variant + state --> <button class="btn btn-primary">Save</button> <button class="btn btn-secondary is-loading">Cancel</button> <!-- Card with sub-elements --> <div class="card"> <div class="card-header">Title</div> <div class="card-body">Content</div> </div> <!-- Table --> <table class="table"> <thead>...</thead> <tbody> <tr class="is-selected">...</tr> </tbody> </table>
{component}
{component}-{variant}
is-{state}
btn
<button>
<a>
<div>
table
<table>
is-*
btn.css
table.css
Style Dictionary is the only required build tool. It reads the W3C DTCG token JSON and outputs:
Build pipeline:
tokens/base/*.json ──┐ tokens/semantic/*.json ──┼── Style Dictionary ──→ web/tokens.css tokens/projects/{name}.json ──┘ ──→ web/projects/{name}.css
Everything else in the system is plain CSS, authored by hand, referencing the generated custom properties. No compilation step needed for reset, base, or component CSS.
Per-project accent generation. Each project has a token override file specifying its accent hue. Style Dictionary generates the full accent scale (base, hover, muted, subtle, text) in OKLCH at build time, output as CSS custom properties:
{ "project": { "accent": { "hue": { "$value": 210 } } } }
Generates:
:root { --color-accent-base: oklch(0.55 0.18 210); --color-accent-hover: oklch(0.48 0.18 210); --color-accent-muted: oklch(0.90 0.05 210); --color-accent-subtle: oklch(0.95 0.025 210); --color-accent-text: oklch(0.45 0.16 210); }
Dark mode accent values are generated alongside, scoped under a [data-theme="dark"] selector or prefers-color-scheme: dark media query (project-level decision which mechanism to use).
[data-theme="dark"]
prefers-color-scheme: dark
Single directory, not npm packages. The design system is a repo with files you import by path. No npm install, no package resolution, no versioning overhead. Projects reference files directly — either via relative paths (if the design system is a git submodule or lives adjacent to the project) or via copied artifacts.
npm install
design-system/ ├── web/ │ ├── tokens.css ← generated by Style Dictionary │ ├── reset.css ← hand-authored │ ├── base.css ← hand-authored │ ├── components/ │ │ ├── btn.css │ │ ├── card.css │ │ ├── table.css │ │ ├── input.css │ │ ├── tag.css │ │ └── ... │ ├── composites/ │ │ └── ... │ └── projects/ │ ├── recipe-manager.css ← generated accent overrides │ └── movie-podcasts.css
How projects consume the system:
The simplest approach: copy the CSS files you need into your project. For Metal, that's just tokens.css. For Simple, it's tokens.css, reset.css, and base.css. Claude Code can handle this copy step during project setup.
reset.css
base.css
For projects that want to stay in sync with the design system without manual copying, git submodules or a simple build script that copies from the design system directory work. The system does not prescribe one method — it depends on how the project is deployed.
Import order matters:
/* Project stylesheet */ @import "tokens.css"; /* 1. Token custom properties */ @import "reset.css"; /* 2. Reset (layer: reset) */ @import "base.css"; /* 3. Base typography (layer: base) */ @import "components/btn.css"; /* 4. Components (layer: components) */ /* 5. Project-specific CSS — no layer, wins by default */
Implementation. Dark mode is handled via a data-theme attribute on the <html> element:
data-theme
<html>
:root { /* light mode tokens */ } :root[data-theme="dark"] { /* dark mode token overrides */ }
Style Dictionary generates both sets of values from the same token source. The dark mode output overrides only the tokens that change — surface colors, text colors, border colors, accent lightness values, and shadow opacity. Spacing, typography, motion, and z-index tokens are identical in both modes.
Theme switching. The system does not include a theme-switching mechanism. Projects that support dark mode implement their own toggle (a button that sets the data-theme attribute and persists the preference to localStorage). A project may also respect prefers-color-scheme as the default with an opt-out toggle. This is a project-level decision.
localStorage
prefers-color-scheme
Rule: All authored CSS (in base, components, and project styles) must reference semantic token custom properties, not raw values. If CSS uses --color-surface-primary instead of #fff, dark mode works automatically when the token value changes.
--color-surface-primary
#fff
Tokens are delivered as a Swift package. Style Dictionary reads the same W3C DTCG JSON source as the web build and outputs Swift extensions. The package provides:
Color
CGFloat
Font
Animation
Naming convention. Swift uses camelCase by convention. Token hierarchy maps with dot notation on the type:
Color.surfacePrimary
Color.accentBase
Color.textSecondary
CGFloat.space4
CGFloat.space8
Per-project accent. Each project specifies its accent hue in its token override file, same as web. Style Dictionary generates Color values for the full accent scale. Additionally, the project sets its accent color via SwiftUI's .accentColor() modifier or the asset catalog's AccentColor so that stock controls (toggles, focus rings, selection highlights) pick it up automatically.
.accentColor()
AccentColor
Dynamic Type is authoritative. On macOS, the system does not enforce the pixel-based type scale from the universal foundation. Instead, it maps typography roles to Dynamic Type text styles, and the platform determines the size.
.serif
.body
.default
.subheadline
.title2
.title
.largeTitle
.caption
.monospaced
Implementation:
// Serif body Text("Article content") .font(.system(.body, design: .serif)) // Sans label Text("Last updated") .font(.system(.caption, design: .default)) .foregroundStyle(Color.textSecondary) // Serif heading Text("Section Title") .font(.system(.title2, design: .serif)) .fontWeight(.bold)
Font design mapping:
No custom fonts are loaded on macOS. The system relies entirely on Apple's built-in type families. This ensures optical consistency with the rest of the OS and gets Dynamic Type accessibility for free.
.system(size: 14)
.fontWeight()
The spacing scale maps directly to CGFloat constants. SwiftUI's padding and spacing use the same 4px base unit / 8px primary step:
VStack(spacing: .space4) { Text("Title") .padding(.bottom, .space2) Text("Body content") } .padding(.space6)
.space1
.space2
VStack
HStack
List
Color delivery. Colors are defined as Color extensions with automatic light/dark adaptation. Style Dictionary generates these from the same token source:
extension Color { static let surfacePrimary = Color("surfacePrimary") // Asset catalog // or static let surfacePrimary = Color(light: .white, dark: Color(hex: "#111111")) }
The preferred approach is asset catalog color sets — each color defined once with light and dark appearances. This gives native dark mode support with no runtime logic. Style Dictionary can generate .colorset directories for the asset catalog as a build step.
.colorset
Dark mode is automatic. macOS handles light/dark switching. The system does not implement custom theme toggling. All views use semantic color tokens (Color.surfacePrimary, not Color.white), and the correct value resolves at render time based on the system appearance.
Color.white
Accent color. The per-project accent hue is set as the asset catalog's AccentColor. Stock controls (toggles, focus rings, sidebar selection, default buttons) automatically use it. Custom views reference Color.accentBase and related tokens.
Stock controls are the default. SwiftUI's built-in controls — Button, TextField, Toggle, Picker, List, Table, NavigationSplitView, Form, DatePicker, Stepper, Menu, DisclosureGroup — already conform to Apple's interaction model, accessibility, keyboard navigation, and animation conventions. The system uses them and styles them through modifiers, not replacements.
Button
TextField
Toggle
Picker
Table
NavigationSplitView
Form
DatePicker
Stepper
Menu
DisclosureGroup
Styling stock controls:
// Button with system styling — inherits accent color automatically Button("Save") { save() } .buttonStyle(.borderedProminent) // TextField with token styling TextField("Search", text: $query) .textFieldStyle(.roundedBorder) .font(.system(.body, design: .serif)) // List with token-based row styling List(items) { item in HStack(spacing: .space3) { Text(item.title) .font(.system(.body, design: .serif)) Spacer() Text(item.date) .font(.system(.caption, design: .default)) .foregroundStyle(Color.textSecondary) } .padding(.vertical, .space2) }
When to build custom views:
A custom view is justified only when:
Custom views must still:
.accessibilityLabel()
.accessibilityRole()
NavigationSplitView for multi-view apps. macOS apps with navigation use NavigationSplitView (sidebar + detail) or NavigationStack for simpler hierarchies. This follows Apple's standard macOS navigation idiom.
NavigationStack
NavigationSplitView { // Sidebar List(sections, selection: $selected) { section in Label(section.title, systemImage: section.icon) } .navigationSplitViewColumnWidth(min: 180, ideal: 220, max: 280) } detail: { // Detail view DetailView(section: selected) }
View structure conventions:
{Feature}View
RecipeListView
ProjectDashboardView
RecipeCard
StatusBadge
struct SerifBody: ViewModifier { func body(content: Content) -> some View { content.font(.system(.body, design: .serif)) } } extension View { func serifBody() -> some View { modifier(SerifBody()) } }
Surface hierarchy in SwiftUI. The three-level surface nesting from the universal foundation maps to:
GroupBox
Color.surfaceSecondary
Color.surfaceTertiary
Use .background() modifier with semantic color tokens. Borders use .overlay() with RoundedRectangle(cornerRadius: 0) (matching the square corners rule) and Color.borderDefault stroke.
.background()
.overlay()
RoundedRectangle(cornerRadius: 0)
Color.borderDefault
Same principle as web: shadows only on floating elements. macOS floating elements are primarily system-managed — popovers, menus, sheets, and alerts get shadows automatically through the windowing system. Custom floating views (if any) use:
.shadow(color: .black.opacity(0.12), radius: 4, x: 0, y: 2) // Dark mode: .shadow(color: .black.opacity(0.4), radius: 4, x: 0, y: 2)
Grounded views (cards, panels, lists) use borders and background color shifts, not shadows. Same rule as web.
Defer to SwiftUI's default animations. The universal foundation's motion principles (functional, restrained, ease-out) align closely with SwiftUI's built-in animation system. The system does not define custom animation curves. Instead:
// Standard state transitions withAnimation(.easeOut(duration: 0.15)) { isExpanded.toggle() } // View transitions .transition(.opacity.animation(.easeOut(duration: 0.25)))
.easeOut(duration: 0.1)
.easeOut(duration: 0.15)
.easeOut(duration: 0.25)
Reduced motion. Respect AccessibilityReduceMotion:
AccessibilityReduceMotion
@Environment(\.accessibilityReduceMotion) var reduceMotion withAnimation(reduceMotion ? .none : .easeOut(duration: 0.15)) { isExpanded.toggle() }
The Swift package can provide animation presets that handle this automatically:
extension Animation { static var dsNormal: Animation { // Returns .none if reduce motion is active // This requires checking at the call site via environment } }
Since @Environment is only available in Views, the practical pattern is a ViewModifier or a convention to always check reduceMotion before animating.
@Environment
reduceMotion
SF Symbols exclusively. On macOS, the system uses SF Symbols for all iconography. No Lucide, no custom icon set.
Image(systemName: "magnifyingglass") .font(.system(size: 16)) .foregroundStyle(Color.textSecondary)
Size values differ slightly from web (16/20px) to align with SF Symbols' optical grid and macOS's text sizing. The sizes are chosen to pair well with the Dynamic Type text styles they appear alongside.
Symbol weight. Use .regular weight to match the overall system weight. Adjust with .symbolRenderingMode(.hierarchical) for multi-layer symbols. Do not use filled variants unless the platform convention calls for it (e.g., sidebar icons use filled by convention in macOS).
.regular
.symbolRenderingMode(.hierarchical)
Swift Package:
design-system/ ├── macos/ │ ├── Package.swift │ └── Sources/ │ └── DesignSystem/ │ ├── Colors.swift ← Generated by Style Dictionary │ ├── Spacing.swift ← Generated by Style Dictionary │ ├── Typography.swift ← Role-to-font mapping │ ├── Animation.swift ← Duration presets │ ├── ViewModifiers.swift ← Convenience modifiers │ └── Assets.xcassets/ ← Generated color sets
Projects add the package as a local dependency:
// Package.swift in the consuming project dependencies: [ .package(path: "../design-system/macos") ]
Then import DesignSystem in any file that needs tokens.
import DesignSystem
Generated vs. hand-authored:
Colors.swift
Spacing.swift
Typography.swift
Animation.swift
ViewModifiers.swift
Assets.xcassets
Placeholder for any additional platforms (iOS, Linux/GTK, CLI). No work needed until a project requires it.
Each platform layer is further refined by project complexity. The complexity tier determines how much of the system a project consumes and how much structural infrastructure is expected. Tiers are not about content volume, visual density, or the number of controls on screen — they are about how much system infrastructure the project needs.
CLI — The system provides conventions for command-line interface structure, not visual tokens. CLI-tier projects are terminal-only tools. The system normalizes command structure (verb-first, canonical verb vocabulary, flag conventions, help format), output styling (color semantics, table formatting, progress indicators), and operational behavior (exit codes, verbosity levels, error routing). No CSS, no visual tokens. If a CLI tool grows a web frontend, the frontend gets its own tier — the CLI portion stays at CLI tier.
Metal — The system provides only CSS custom properties. No reset, no base typography styles, no component patterns. You import the token file and write your own HTML/CSS directly against the values. There is no build step beyond the token import. The token file is a few KB at most. Use this when shipping even a CSS reset is unnecessary overhead.
Simple — The system provides tokens plus the base layer: reset, typographic styles, prose formatting, and base styling for native HTML controls (inputs, selects, buttons, tables). Semantic HTML looks correct out of the box. Interactions are stateless and local — filtering a list, sorting a table, toggling a view. No persistent client state, no data mutation, no multi-step flows. A Simple project may have many pages and real UI variety, but structurally it has few templates and its JS is minimal. The difference between Metal and Simple is whether the project benefits from the system styling HTML for you.
Standard — The system provides the base layer plus a structured component library (buttons, inputs, cards, navigation, tables, form groups, tags, status indicators). Multiple distinct view types exist and state flows between them — a list view and a detail view, a search that persists, data that can be created/edited/deleted. Standard components are used in standard ways. If every control in the project exists in the component library and is used without significant modification, the project is Standard.
Full — The complete system including composite patterns (dashboard layouts, settings panels, data tables with inline editing, multi-panel views) and rules for building custom controls. The project requires interaction patterns that don't exist in the standard component library — drag-and-drop, command palettes, real-time data streams, keyboard-driven workflows, or novel interactive elements. If the project needs a control that must be designed and specified from scratch, it is Full.
Tier assignment is a judgment call, not a formula. The following questions guide the decision:
A project's tier should be assigned at the start and recorded in its CLAUDE.md. Tiers can change — a project that starts as Simple may graduate to Standard as scope expands. When this happens, the CLAUDE.md is updated and the project adopts the additional system layers.
Metal projects are only bound by the token values themselves. They must use system spacing, colors, and fonts, but they are not required to follow the interactive state model, motion principles, or role assignments — the assumption is that a Metal project is too minimal to need them. If a Metal project finds itself needing interactive states or typography roles, it should graduate to Simple.
CLI projects are bound by the command structure, output styling, and operational conventions defined in Section V. They are not bound by visual tokens, interactive state models, or any web/macOS platform rules.
The tier model applies to macOS/SwiftUI projects with platform-appropriate assets.
What each tier ships (macOS):
Metal — import DesignSystem gives you Color extensions and CGFloat spacing constants. You write all views from scratch using stock controls and apply token values manually.
Simple — Adds the typography helpers (serifBody(), role-to-font mapping), animation presets, and convenience ViewModifiers. Stock controls styled through modifiers. Single-window tools with limited navigation.
serifBody()
Standard — Adds reusable view patterns (card views, list row styles, table configurations, form layouts) as SwiftUI Views or ViewModifiers in the package. Multi-view apps with NavigationSplitView, persistent state, data mutation.
Full — Adds composite view patterns (dashboard layouts, inspector panels, multi-pane configurations) and conventions for building custom controls. Complex apps with novel interaction patterns.
Some projects don't build UI — they translate design system tokens into the styling format required by a third-party library. These are token translators: small, Metal-tier projects that sit at the boundary between the design system and an external system's configuration format.
Examples:
Characteristics of a token translator:
Token translators in the repo:
design-system/ ├── translators/ │ ├── mapbox-theme/ │ │ ├── CLAUDE.md ← Metal tier, describes the mapping logic │ │ ├── build.js ← Reads tokens, outputs style.json │ │ └── style.json ← Generated artifact │ ├── syntax-theme/ │ └── chart-theme/
The meta-rules describe how to extend the system. They are the primary interface between the design system and AI-assisted development tools (Claude Code). A project's CLAUDE.md references the relevant rule sets so that the agent can build new components, layouts, and patterns that are system-compatible without every possibility being enumerated in advance.
Token names use dot-separated hierarchy in the W3C DTCG source files:
CSS custom properties mirror token names with dashes:
--color-accent-base
--space-4
--text-lg
--font-serif
CSS classes for components use flat, readable names. No BEM, no utility class methodology:
card
input
tag
badge
timeline
dropdown
btn-group
spinner
btn-primary
btn-secondary
btn-accent
card-compact
is-active
is-disabled
is-loading
is-selected
stack
row
grid
sidebar
The pattern is: {component} for the base, {component}-{variant} for variants, is-{state} for states. No nesting in names — card-label not card__label.
card-label
card__label
Project-local CSS classes use a short prefix derived from the project name (2–3 lowercase letters, dash-separated from the class name). This prevents collisions with current and future shared component names.
{prefix}-{name}
{prefix}-{name}-{variant}
mp-
mp-badge
mp-card-info
mp-episode-card
CSS prefix:
Component files are named to match their primary class: btn.css, card.css, table.css. One component per file.
card.css
Design system repo directory structure:
design-system/ ├── tokens/ │ ├── base/ # Raw values (scales, palettes) │ ├── semantic/ # Semantic aliases (surface, text, border) │ └── projects/ # Per-project overrides (accent hue) │ ├── recipe-manager.json │ └── movie-podcasts.json ├── rules/ │ ├── universal/ # Declaration + universal foundation rules │ ├── web/ # Web platform rules │ └── macos/ # macOS/SwiftUI platform rules ├── web/ │ ├── tokens.css # Generated — do not edit │ ├── reset.css # CSS reset │ ├── base.css # Typography, prose, native control styling │ ├── components/ # Component CSS files │ │ ├── btn.css │ │ ├── card.css │ │ ├── table.css │ │ └── ... │ └── composites/ # Composite pattern CSS files ├── macos/ │ └── Sources/ # Swift package ├── specs/ # Platform-agnostic component specifications │ ├── btn.md │ ├── card.md │ └── ... └── templates/ └── CLAUDE.md.template # Template for new project CLAUDE.md files
Naming rules:
btn-black
Every shared component has a platform-agnostic specification that describes what the component is — its purpose, states, behaviors, and accessibility contract — independent of any implementation. Platform implementations reference this spec.
Spec template (Markdown):
# Component: {name} ## Purpose One sentence describing what this component is for. ## Anatomy - List of sub-elements (e.g., label, icon, indicator) - Which are required vs. optional ## Variants - {variant-name}: description and when to use ## States Which of the canonical states apply to this component: - Rest, Hover, Focus, Active, Disabled, Loading, Selected (if applicable) - Any state-specific behavior beyond the universal rules ## Tokens Used - List of design tokens this component consumes - e.g., --space-2, --color-accent-base, --text-sm, --font-sans ## Accessibility - Required ARIA role or semantic HTML element - Keyboard interaction pattern - Minimum contrast requirements (if beyond the system default) - Screen reader announcement behavior ## Behavior - Interaction details not covered by the universal state model - e.g., "dropdown closes on outside click", "tooltip appears after 200ms delay" ## Platform Notes - Web: any CSS-specific considerations - macOS: any SwiftUI-specific considerations
Global CLAUDE.md (~/.claude/CLAUDE.md):
~/.claude/CLAUDE.md
The global file contains a standing instruction that applies to all projects. It should include:
## Design System When working in any project: 1. If no CLAUDE.md exists in the project root, create one from the design system template before doing other work. Ask me to confirm the tier assignment (Metal, Simple, Standard, Full) before proceeding. 2. All UI work must follow the design system rules for the assigned tier. Reference the design system repository at {path-or-url} for token values, component specs, and rules. 3. Never use arbitrary color values, spacing values, or font stacks. All values come from the design system tokens. 4. When building a new UI element that does not exist in the component library, follow the meta-rules for construction: use system tokens, follow the naming conventions, match the interactive state model, and build it so it could be upstreamed later.
Project CLAUDE.md template:
# {Project Name} ## Design System - Tier: {Metal | Simple | Standard | Full} - Accent hue: {0-360} - Design system: {path-or-url to design system repo} ## Architecture {Describe the app architecture — static site, SPA, server-rendered, etc.} ## Platform {Web | macOS | Web + macOS} ## Key Views {List the primary views/templates and their purpose} ## Testing {Testing approach and tools} ## Project-Specific Rules {Any rules that apply only to this project, beyond the design system}
How the CLAUDE.md communicates tier:
The tier assignment tells Claude Code exactly which system layers to use:
The hybrid model: components start as project-local, built following system conventions, and are promoted to the shared library when mature.
Step 1: Build locally, build compatibly.
When Claude Code builds a new UI element in a project, it follows the system's conventions even though the element is project-local:
Because the agent follows these rules (via the CLAUDE.md), the resulting component is system-compatible from birth. No migration step is needed later.
Step 2: Identify promotion candidates.
A project-local component is a candidate for promotion when:
This is a judgment call made by the builder (you), not an automated process.
Step 3: Write the abstract spec.
The first step of promotion is writing the platform-agnostic component spec (Section IV.B). Claude Code can draft this from the existing implementation — the spec describes what the component does, its states, tokens, and accessibility contract, abstracted from the specific CSS/HTML.
Step 4: Move to the shared library.
web/components/
macos/Sources/
specs/
Step 5: Announce availability.
No formal announcement process needed for a single-builder system. The component exists in the shared repo; Claude Code will find it via the directory structure and specs. Other projects pick it up when their CLAUDE.md references the system and the agent recognizes an opportunity to use it.
Token compliance linting:
Automated checks that no arbitrary values appear in project CSS. A linter scans for:
This can be implemented as a simple CLI tool (grep/regex-based for v1, a proper CSS AST parser later) that runs against project stylesheets.
Tier compliance checking:
A separate linter pass validates that a project's code does not exceed its assigned tier. The project's CLAUDE.md declares the tier; the linter checks for violations:
When a violation is detected, the linter produces one of two recommendations:
The linter does not auto-fix — it flags the mismatch and lets the builder decide which direction to resolve. This is important because both directions are valid: sometimes you accidentally over-imported, and sometimes the project has legitimately outgrown its tier.
When Claude Code encounters a tier compliance warning, it should pause and ask the builder which resolution to take before proceeding.
Pattern detection linting:
A third linter detects project CSS that reimplements a design system component instead of using the shared component. This addresses a gap the other two linters cannot catch: a project might use only valid tokens (passing token compliance) at a tier that permits components (passing tier compliance) while still hand-building a spinner, modal, or dropdown from scratch instead of importing the existing component.
The linter runs a 3-pass pipeline against Standard and Full tier projects only (components are not available below Standard):
Pass 1 — Code Similarity. Builds a fingerprint for each DS component by reading its CSS file and spec. The fingerprint captures class names, token references, CSS properties, distinctive property combinations, layout values, and animation patterns. Each project CSS rule is scored against every fingerprint on five signals: name similarity (after prefix stripping), token overlap (Jaccard), property overlap (Jaccard), property combination match, and animation pattern match. A cluster bonus rewards cases where multiple project classes collectively cover a component's anatomy (e.g. .gp-modal, .gp-modal-header, .gp-modal-body matching the modal's structure).
.gp-modal
.gp-modal-header
.gp-modal-body
Pass 2 — Purpose Matching. Hardcoded structural heuristics detect CSS property+value combinations that indicate a known component purpose, independent of naming or token usage. Examples: border-radius: 50% + animation: infinite + border-top-color → spinner; position: fixed + inset: 0 + z-index + centered flex → modal; position: absolute + display: none + box-shadow + z-index → dropdown. HTML is also scanned for ARIA patterns (role="menu", role="dialog", role="status").
border-radius: 50%
animation: infinite
border-top-color
position: fixed
inset: 0
z-index
position: absolute
display: none
role="menu"
role="dialog"
role="status"
Pass 3 — LLM Validation. When --llm is passed, top candidates from passes 1 and 2 are sent to an LLM (Haiku) with the component spec and CSS for a confidence-scored validation. The LLM returns a verdict (reimplementation, partial, or unrelated) with reasoning. Requires ANTHROPIC_API_KEY in the environment; gracefully skips if missing.
--llm
reimplementation
partial
unrelated
ANTHROPIC_API_KEY
Scores from all passes are combined: max(pass scores) + 0.3 × sum(remaining), capped at 100. Results are labeled HIGH (80–100), MEDIUM (60–79), or LOW (40–59). The linter does not auto-fix — it surfaces candidates with evidence so the builder can decide whether to adopt the DS component or keep the project-local implementation.
max(pass scores) + 0.3 × sum(remaining)
Exit codes: 0 = no candidates (clean), 1 = candidates found, 2 = configuration error. Run via npm run lint:patterns -- <project-path> or node tools/lint-patterns.mjs <project-path>.
npm run lint:patterns -- <project-path>
node tools/lint-patterns.mjs <project-path>
Visual reference pages:
Each project at Simple tier or above should have a reference page — a single HTML page (like the specimens in this document) that renders all UI components and patterns used in that project, in both light and dark mode. This page serves two purposes:
Visual regression (screenshot diffing):
For Standard and Full tier projects:
This is not needed for Metal or Simple projects — the investment isn't justified for the complexity level.
Accessibility auditing:
For all tiers at Simple and above:
npm run lint:a11y
node tools/lint-a11y.mjs <file ...>
node tools/lint-a11y.mjs --project <path>
docs/accessibility.md
How evaluation integrates with Claude Code:
The CLAUDE.md can include a rule like:
## Quality Checks Before considering UI work complete: 1. Run the token compliance linter against all modified CSS 2. Run the pattern detection linter to check for reimplemented components 3. Run the accessibility linter against all modified HTML 4. Verify the reference page renders correctly in both light and dark mode 5. For Standard/Full: run visual regression checks 6. Fix any failures before marking the task done
This makes quality evaluation part of the agent's workflow, not a separate step the builder has to remember.
CLI-tier projects are terminal-only tools. This section defines the conventions that replace visual tokens for the command-line context. Every CLI-tier project must follow these rules.
Commands follow a verb-first pattern:
toolname verb [noun] [flags]
Single-purpose tools may omit the verb (the tool name is the verb): lint-tokens [project-path].
lint-tokens [project-path]
Canonical verb vocabulary:
add
remove
list
show
search
sync
build
lint
config
status
run
init
Use these verbs when they fit. Invent new verbs only when no canonical verb covers the action. Never alias a canonical verb to a different word (e.g., don't use create when add fits, don't use check when lint fits).
create
check
Universal flags — every tool must support:
--help
-h
--version
-v
Standard optional flags:
--verbose
-V
--quiet
-q
--dry-run
--color
--no-color
--json
Flag rules:
--kebab-case
-p
--verbose=true
--flag value
--flag=value
1
2
Empty results are success — list with no items exits 0, not 1. Linters exit 1 when violations are found (violations are the "error" the linter reports). A tool that cannot find its config file exits 2, not 1.
Channel discipline:
Never mix data and diagnostics on the same channel.
Color semantics:
Color augments text — never use color as the sole indicator of meaning. Every colored message must be readable without color.
Color environment rules:
NO_COLOR
Table formatting:
Machine-readable output:
Progress indicators:
toolname — One-line description of what the tool does Usage: toolname verb [noun] [flags] Commands: add Create a new record remove Delete a record list Display all records Flags: -h, --help Show this help -v, --version Show version -V, --verbose Enable debug output -q, --quiet Suppress non-error output Examples: toolname list --json toolname add "my item" --verbose
—
~/.config/toolname/config.toml
~/.toolname/config.toml
toolname config
toolname config get key
toolname config set key value
Error: <message> <optional context line> <optional context line>
Error:
Warning:
Info:
Error: Missing required field "name" at config.toml:3 Error: Invalid value for "port": must be 1-65535 at config.toml:7 2 errors found.