Book a callGitHub1

Guides03/10

For engineers

Updated · Edit on GitHub

OpenDesigner turns design decisions into files you import: DTCG 2025.10 tokens, CSS variables, a Tailwind v4 theme, Swift and Jetpack Compose files, and a DTCG resolver. engine.py review finds values typed straight into your code that skip the tokens. DESIGN.md and a short note in AGENTS.md tell coding agents to use the tokens too.

What you get

All paths are inside opendesigner/ in your project.

  • tokens/: the source of truth, in DTCG 2025.10 format. Light and dark, three densities and reduced motion are modes, tied together by opendesigner.resolver.json.
  • build/css/tokens.css: CSS custom properties, named --ds-....
  • build/tailwind/theme.css: a Tailwind CSS v4 theme.
  • build/swift/DesignTokens.swift: SwiftUI values under DS.
  • build/compose/DesignTokens.kt: Jetpack Compose values.
  • build/dtcg/: one resolver file for tools that take a single entry point.

Never edit an export by hand. Change the decision and rebuild, and every export updates together.

Wire it in

Plain CSS

Import the file once at the root of your app, then use the variables:

@import "../opendesigner/build/css/tokens.css";

.card {
  background: var(--ds-color-surface-raised);
  color: var(--ds-color-text-primary);
}

Dark mode follows prefers-color-scheme. Set data-theme="light" or data-theme="dark" to override it. Density switches with data-density (spacious, comfortable or compact). Reduced motion follows prefers-reduced-motion, with a data-motion override. Font sizes are in rem, so browser text zoom works.

A value typed in by hand

It looks right today. It skips dark mode, density and the next rebuild, and engine.py review flags all three.

.card {
  background: #f3f4f6;
  border-radius: 7px;
  padding: 13px;
}
7 is not a radius in the system and 13 is off the 4px ladder.

The token for the role

The value follows the decision. Change the decision and rebuild, and this card changes with every other one.

.card {
  background: var(--ds-color-surface-sunken);
  border-radius: var(--ds-radius-container);
  padding: var(--ds-space-12);
}
The same three tokens review names for the raw values on the left.
The card from the review example, before and after. Each raw value has a token for its role, and the token keeps working in dark mode, in every density and after every rebuild.

Tailwind v4

Add two lines to your main CSS file, for example src/index.css. Adjust the path to where that file sits:

@import "tailwindcss";
@import "../opendesigner/build/tailwind/theme.css";

theme.css imports tokens.css itself, so modes keep working. Classes follow the token roles, such as bg-action-primary, text-fg-primary, rounded-control and h-control-md. DESIGN.md lists the class for each role.

By default, the export switches off Tailwind's own colors, text sizes, radii and shadows. That way a stray bg-blue-600 or rounded-lg doesn't compile. To keep them, run engine.py set exports.tailwindReset false, then engine.py build.

iOS

Add DesignTokens.swift to your iOS target. Colors switch with the system appearance, for example DS.Colors.textPrimary. Text uses Dynamic Type text styles, so it follows the person's text size setting.

Android

Add DesignTokens.kt to your Android UI module. It holds light and dark color sets plus spacing, type and motion values.

If the brand font's licence doesn't cover apps, the Swift and Compose files use the system font instead, and say so.

Token tools

Point your token tool at tokens/opendesigner.resolver.json, or at the single file in build/dtcg/. The export skill names Style Dictionary, Terrazzo, Tokens Studio and Penpot as tools that read this format. We have not tested any of them yet.

Add a value the system lacks

First check that no existing token fits. If none does, add one through the engine with a reason:

engine.py set size.row.md 36 --why "table rows in the jobs list"
engine.py set color.bg.highlight '"#fff4c2"' --why "highlighted search matches"
engine.py set dark:color.bg.highlight '"#3a3000"' --why "the same highlight in dark mode"
engine.py build

A new color still has to pass the contrast check. build reports it if it doesn't.

Find hard-coded values

Run this at the end of any piece of work:

engine.py review --project src

It reads style sheets, JavaScript and TypeScript, Vue, Svelte, HTML, Swift and Kotlin files. It finds colors, sizes, radii, shadows and durations typed in by hand and names the token to use instead. Here is part of the output from a test run:

card.css:1  #f3f4f6  ->  use var(--ds-color-surface-sunken)
card.css:1  border-radius: 7px  ->  use var(--ds-radius-container) (12px, the container radius)
card.css:1  padding: 13px  ->  use var(--ds-space-12) (13 is off the 4px ladder)

It also lists code that breaks a standard, and DESIGN.md sections that are out of date. Add the comment od-ignore to a line to keep a raw value on purpose. --strict makes the command fail when it finds anything, and --json prints machine-readable output.

$ engine.py review --project srctest run
Review of src: 1 files scanned.
3 hard-coded values skip the tokens: 1 color, 1 radius, 1 size.
card.css:1 #f3f4f6 -> use var(--ds-color-surface-sunken)
card.css:1 border-radius: 7px -> use var(--ds-radius-container) (12px, the container radius)
card.css:1 padding: 13px -> use var(--ds-space-12) (13 is off the 4px ladder)
To keep a raw value on purpose, add the comment od-ignore on that line.
1 line breaks a standard:
card.css:1 padding: 13px -> Write spacing in rem or em so the layout scales with the user's text size. (STD-accessibility-motion-13, must)
Fix the code. A house standard changes only when the person asks: engine.py standard override <id> --why "<their words>".
DESIGN.md matches state.json.
The whole output of engine.py review on a one-line style sheet: three raw values with the token to use for each, one line that breaks a house standard, and a DESIGN.md that is still in step with state.json. Photo: Eduardo Drapier on Unsplash.

DESIGN.md and AGENTS.md for coding agents

DESIGN.md has a section for AI agents. It tells them to read the file and the tokens before changing any screen, to use tokens instead of raw values, and to leave locked decisions alone.

DESIGN.md · For Agents
Read this file and the tokens before you change any screen.
1Before any visual work, read this file, PRODUCT.md and opendesigner/tokens/.
2Use tokens, never raw values: CSS var(--ds-...), Tailwind classes from theme.css, DS.* in Swift.
3Do not change a locked decision without asking the owner.
4To change the system: engine.py set <path> <value> --why "...", then generate, validate and design-md.
5To extend: add a decision, never hand-edit tokens or this file.
6After any implementation, run engine.py review and re-read this file.
7Follow the Standards section. Change a standard only when the person explicitly asks.
same rules in AGENTS.mdadded only after you say yes
The For Agents section at the end of DESIGN.md, as OpenDesigner's own brand file has it: read first, tokens only, locks untouched, decisions instead of hand edits, and a review at the end. The optional AGENTS.md note repeats it for every later session. Photo: Harshal Tandel on Unsplash.

At the end of a session, OpenDesigner asks whether to add a short note to your AGENTS.md. It adds it only after you say yes. The note tells later agents to:

  • read DESIGN.md and PRODUCT.md before any visual work;
  • use the tokens or their exports, and never hard-code colors, sizes, spacing, radii, shadows or durations;
  • check opendesigner/decisions.md for why a value is what it is;
  • run engine.py review at the end of every implementation and fix what it finds.

About the engine

The engine is one Python file that needs Python 3.10 or newer. It uses only the standard library and makes no network calls. The same state.json gives the same tokens. It has 157 tests, which pass on the maintainer's computer. No CI runs are recorded yet.

Not yet

  • No direct React Native or Flutter export.
  • Only buttons, a text field and a card are previewed as components.

See Engine commands and Export for more.