# What DTA is URL: https://dta.martinprinz.dev/start/ Design Token Authority (DTA) is a command-line tool for design tokens. It reads your tokens as plain [DTCG](https://tr.designtokens.org/format/) JSON files (pulled from Figma or written by hand), checks their structure, builds them into CSS, JavaScript and Tailwind, and shows what a change does to your UI before it ships. Every command is deterministic: same input, same result, on your machine, in CI or when an agent runs it. ## Why automate A one-off export from Figma is easy: a plugin, a copy, done. It gets hard when tokens change every week: - **Structure drifts.** New variables break naming and layering, and nobody notices until a theme breaks. - **Code breaks quietly.** A renamed or deleted token is still referenced somewhere. - **Screenshots pile up.** One colour change touches a hundred screens, and nobody reviews a hundred screenshots. DTA turns each change into something you can review: the token diff, the lint findings and the visual difference, grouped by the token that caused it. | You need… | DTA gives you | | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | tokens out of Figma once, for a prototype | `dta pull` (or `--from-file`) and `dta build` | | the same tokens in code on every change | `dta build` in CI, with `dta lint` failing on structural errors | | to see the effect of a change before it merges | `dta diff`: base and head rendered and compared, explained by token | | tokens back in Figma after a code-side change | `dta push`, conservative: it creates and updates, it never deletes; `dta prune` deletes what has no token any more, when you ask | ## What it does today | Command | What it does | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `dta init` | Wizard that writes `dta.config.ts` for your Figma plan and outputs, and sets up `dta diff` (with a config: sets it up or updates it) | | `dta pull` | Figma variables → token files (REST API, or `--from-file` with a plugin export) | | `dta push` | Token files → Figma variables, with a dry run and a typed confirmation | | `dta prune` | Deletes Figma variables whose tokens are gone (dry run, typed confirmation; push never deletes) | | `dta build` | Token files → CSS custom properties, JavaScript, Tailwind v3 and v4; Swift, Android XML and Compose when listed in `outputs.formats` | | `dta lint` | Structure checks: dangling and circular aliases, default mode names, configurable rules | | `dta graph` | The alias graph: depth, orphans, cycles; console, Markdown, DOT or interactive HTML | | `dta diff` | Token changes against a base, and screenshots of your stories explained by token | | `dta analyze` | Which layer each Figma collection plays (primitives, brand, dimension) | | `dta clean` | Removes token files and build output | Breaking-change detection in code is planned; this page lists only what is released in the CLI. No install yet? The [playground](/playground/) shows the structure DTA detects in your token files (or a plugin export), what `dta lint` finds, and the dependency graph per brand and breakpoint (each token's alias chain as `dta token get` reads it), builds the outputs `dta build` writes (CSS, Tailwind, JS) to copy or download, one file or all of them as a zip, and generates the `dta.config.ts` that gives the same files with the CLI. Its "Example with problems" shows what a finding looks like. It all runs in your browser: nothing is uploaded. The page opens with only the input; the code that reads your files follows in the background, and if you are quicker than it, the summary says `Reading 6 files…` for a moment. Next: [Quick start](/start/quick-start/). --- # Quick start URL: https://dta.martinprinz.dev/start/quick-start/ :::caution[`npx dta` is another package] The npm name `dta` belongs to an unrelated package. Run `npx design-token-authority …` until DTA is installed in your project (`npm create dta` installs it); after that, `dta` works in package scripts and `npx dta` finds it. ::: The examples on this page ran on the `brand-ab` fixture of the repository (`test/fixtures/brand-ab`): a Primitives collection, a Brand collection with the modes BrandA and BrandB, and ScreenType with Desktop, Tablet and Mobile. The output is copied from those runs. 1. **Create a config.** In your project folder, `dta init` asks which Figma plan you have, which collections and brands to build and which outputs you want, and writes a `dta.config.ts` (`dta init --yes` asks nothing: it writes the config for exported token files in `tokens/` with every output format, for CI and coding agents). Your outputs become `outputs.formats`, so `dta build` writes only those; picking CSS and Tailwind v4 gives: ```ts title="dta.config.ts (excerpt, as dta init writes it)" outputs: { formats: ['css', 'tailwind-v4'], css: { outDir: 'build/css', }, tailwind: { outDir: 'build/tailwind', }, }, ``` iOS and Android can be picked too: they become `ios-swift`, `android-xml` and `android-compose` ([Native outputs](/reference/outputs/#native-outputs)). The fixture's config builds all four outputs (no `formats`) and looks like this: ```ts title="dta.config.ts" export default { figma: { source: 'file' }, tokens: { dir: 'tokens' }, brands: ['BrandA', 'BrandB'], breakpoints: { collection: 'ScreenType', default: 'Mobile', modes: { Tablet: '(min-width: 768px)', Desktop: '(min-width: 1024px)' }, }, outputs: { css: { outDir: 'output/css' }, tailwind: { outDir: 'output/tailwind' }, }, } ``` `breakpoints` turns the `ScreenType` modes into media queries in `variables.css`; `dta init` does not write it, because the queries are yours to choose ([Breakpoint modes](/reference/outputs/#breakpoint-modes)). 2. **Get your tokens.** On a Figma Enterprise plan, `dta pull` reads the variables through the REST API. On any other plan, export them with a plugin and import the file; see [Getting tokens out of Figma](/guides/figma/). The result is one JSON file per collection and mode in `tokens/`: ```text tokens/ Brand(Alias).BrandA.json Brand(Alias).BrandB.json Primitives.Value.json ScreenType.Desktop.json ScreenType.Mobile.json ScreenType.Tablet.json ``` 3. **Build.** ```text $ dta build T ⚒ dta build | o Base layer complete | o Brand BrandA complete | o Brand BrandB complete | o Shared outputs complete | o Generated files ---------------------------+ | | | output/css/base.css | | output/css/themes/branda.css | | output/tailwind/branda/tailwind.tokens.ts | | output/tailwind/branda/tailwind.css | | output/css/themes/brandb.css | | output/tailwind/brandb/tailwind.tokens.ts | | output/tailwind/brandb/tailwind.css | | output/css/variables.css | | output/js/colorpalette.js | | output/tailwind/tailwind.tokens.ts | | output/tailwind/tailwind.css | | | +---------------------------------------------+ | — Build complete! ``` What each file is: [Outputs](/reference/outputs/). 4. **Check the structure.** With no rules configured, `dta lint` still catches dangling and circular aliases and Figma's default mode names. The fixture enables more rules, so it reports warnings: ```text $ dta lint … | Primitives.Value.json | | warn Duplicate value "8" — also defined in: Primitives.Value/Dimensions/spacing/2 [no-duplicate-values] | | 8 warnings found. | — Lint passed with warnings. ``` Warnings exit with 0, errors with 2: [Lint rules](/reference/lint-rules/). 5. **See what changed.** `dta diff` compares your tokens with a base: a folder, a git ref, or by default the merge base with your main branch. ```text $ dta diff --tokens-only --base tokens-base T ◑ dta diff | | Base: tokens-base changed Colors.Brand.Accent Brand(Alias) · BrandB {Colors.Foundation.Navy.700} → #2c4466 → {Colors.Foundation.Sky.500} → #3b8fd9 changed Colors.Foundation.Blue.500 Primitives · Value #0047a0 → #0057b8 changed Dimensions.spacing.8 Primitives · Value 28 → 32 added Dimensions.radius.pill Primitives · Value 999 removed Colors.Foundation.Steel.500 Primitives · Value #7d8ea3 affected Colors.Brand.Primary Brand(Alias) · BrandA {Colors.Foundation.Blue.500} → #0047a0 → {Colors.Foundation.Blue.500} → #0057b8 (because of Colors.Foundation.Blue.500) affected Spacing.Padding ScreenType · Desktop {Dimensions.spacing.8} → 28 → {Dimensions.spacing.8} → 32 (because of Dimensions.spacing.8) | — Tokens: 3 changed · 1 added · 1 removed · 2 affected ``` With a `diff` section in the config it also screenshots your stories at the base and the head and explains each change by token. 6. **Review UI changes in pull requests.** `dta init` sets this up: as the last question of the wizard in a new project, and straight away in a project that already has a config (`dta init --diff` runs only this step). It finds your Storybook (or asks which pages to check), package manager, Node version and default branch, says where each came from, and writes the `diff` section, the GitHub workflow and `playwright-core`. On a project with Storybook (`--yes` takes the detected and default answers): ```text $ dta init --diff --yes T ✨ dta init | o Found | | Storybook build-storybook → storybook-static, from the "build-storybook" script in package.json | Package manager npm, from package-lock.json | Node 22, from .nvmrc | Build npm run build-storybook, from the "build-storybook" script in package.json | Git host GitHub, from the origin remote | Default branch main, from the local branches | o Will write --------------------------------------------+ | | | build: npx dta build && npm run build-storybook (new) | | outDir: storybook-static (new) | | stories: Storybook (new) | | workflow: split (new) | | .github/workflows/dta-diff.yml (new) | | .github/workflows/dta-diff-report.yml (new) | | .github/workflows/dta-diff-approve.yml (new) | | | +---------------------------------------------------------+ | * Wrote the diff section of dta.config.json | * Wrote .github/workflows/dta-diff.yml | * Wrote .github/workflows/dta-diff-report.yml | * Wrote .github/workflows/dta-diff-approve.yml | o Ran npm install --save-dev playwright-core | o Next -------------------------------------------+ | | | npx dta diff --check checks the setup | | npx dta diff renders a report locally | | Then open a pull request. | | | +--------------------------------------------------+ | — dta diff is set up. ``` Then check the setup and open a pull request: ```text $ dta diff --check ✓ config diff section: outDir storybook-static, build `npx dta build && npm run build-storybook` ✓ browser DTA_BROWSER_PATH (/usr/bin/chromium) ✓ git history full history ✓ base merge base with main (c320792) ✓ stories 1 story in storybook-static/index.json Ready for dta diff. ``` More in [dta diff in pull requests](/guides/dta-diff-in-pull-requests/). Next: [Core concepts](/start/concepts/). --- # Core concepts URL: https://dta.martinprinz.dev/start/concepts/ ## Collections, modes and token files DTA follows Figma's variable model. A **collection** groups variables; a **mode** is one set of values for all of them (a brand, a theme, a breakpoint). On disk, every collection and mode is one JSON file named `{Collection}.{Mode}.json`: ```text tokens/Primitives.Value.json one collection, one mode tokens/Brand(Alias).BrandA.json one collection, a mode per brand tokens/Brand(Alias).BrandB.json tokens/ScreenType.Desktop.json one collection, a mode per breakpoint ``` Inside, the files use the [W3C Design Tokens (DTCG)](https://tr.designtokens.org/format/) format: nested groups, and tokens with `$type` and `$value`. The full format: [Token files](/reference/token-files/). ## Aliases and unique names A value in braces points at another token: `"$value": "{Colors.Foundation.Blue.500}"`. In Figma the same path reads `Colors/Foundation/Blue/500`; DTA converts between the two when it pulls and pushes. Aliases carry no collection name, so **variable names must be unique across all collections**. That is what lets DTA resolve `{Colors.Foundation.Blue.500}` wherever it lives, and it is the one rule DTA asks your Figma file to follow. `dta graph` shows the alias structure, and `dta lint` fails on aliases that point nowhere or in a circle. ## Layers Most token sets end up with layers, whatever they call them. DTA works with any names, but its checks and defaults speak this model: | Layer | Example collection | Modes | Values | | -------------- | ------------------ | ------------------ | ---------------------------------------- | | **Primitives** | `Primitives` | one | raw values: `#0057b8`, `8` | | **Brand** | `Brand(Alias)` | one per brand | aliases into the primitives | | **Dimension** | `ScreenType` | one per breakpoint | aliases or values that differ per screen | `dta analyze` reads a Figma file and suggests which collection plays which role, and `dta init` uses that to propose `collections` and `brands`. The `semantic-must-alias` lint rule keeps a brand layer made of aliases only. ## What the build makes of it `dta build` writes one set of files for the single-mode collections and one theme per brand: - **Single-mode collections** (the primitives) become `:root` custom properties in `variables.css` (aliases stay `var()` references) and `base.css` (values resolved). - **Each brand** listed in `brands` becomes `themes/.css`, scoped to `[data-brand=""]`. Switch brands by setting the attribute on any element, `` for the whole page. - **The breakpoint collection** named in `breakpoints` is added to `variables.css`: its default mode on `:root`, the other modes inside their media queries. - **JavaScript and Tailwind** files are written next to them. ```css title="output/css/themes/branda.css" [data-brand="branda"] { --colors-brand-primary: var(--colors-foundation-blue-500); --colors-brand-primary-dark: var(--colors-foundation-blue-700); --colors-brand-accent: var(--colors-foundation-sky-500); … } ``` :::note[Breakpoint modes] A collection whose modes are screen sizes (such as `ScreenType` with `Desktop`, `Tablet` and `Mobile`) needs its media queries from you: name it in `breakpoints` with a default mode and a query per other mode, and `dta build` writes it as media blocks ([Outputs](/reference/outputs/#breakpoint-modes)). Without that section it is read, linted, diffed and pushed, and only resolves aliases in the build (through its first mode in alphabetical order). ::: Every file and its format: [Outputs](/reference/outputs/). ## Config The project config is `dta.config.ts` (also `.mts`, `.js`, `.mjs` or `.json`). Unknown keys are rejected, so a typo shows up as an error instead of being ignored: ```ts title="dta.config.ts" export default { figma: { source: 'file' }, // 'api' on an Enterprise plan tokens: { dir: 'tokens' }, collections: ['Primitives', 'Brand(Alias)', 'ScreenType'], // default: all brands: ['BrandA', 'BrandB'], breakpoints: { collection: 'ScreenType', default: 'Mobile', // on :root modes: { Tablet: '(min-width: 768px)', Desktop: '(min-width: 1024px)' }, }, outputs: { formats: ['css', 'tailwind-v4'], // default: all four ('tailwind-v3', 'js' too) css: { outDir: 'output/css' }, tailwind: { outDir: 'output/tailwind' }, }, lint: { rules: { 'semantic-must-alias': { severity: 'error', collections: ['Brand(Alias)'] } } }, } ``` Once DTA is installed as a package, `defineConfig` from `design-token-authority` wraps the object for editor completion; it changes nothing else. Every key, with its type and default: [Configuration](/reference/configuration/). A `dta.config.json` can point `"$schema"` at [`/schemas/dta.config.schema.json`](/schemas/dta.config.schema.json) on this site for completion in editors. --- # The DTCG format URL: https://dta.martinprinz.dev/learn/ DTA's token files use the format of the W3C [Design Tokens Community Group](https://www.designtokens.org/) (DTCG). Its first stable version, [2025.10](https://www.designtokens.org/TR/2025.10/), has three modules. The spec is the reference; this page only gives the ideas and says where DTA differs. | Module | What it defines | | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Format](https://www.designtokens.org/tr/2025.10/format/) | Tokens (objects with `$value`) and groups, `$type`, `$description`, `$extensions`, `$deprecated`, aliases (`{group.token}`, `$ref`), `$root`, `$extends`, and the types | | [Color](https://www.designtokens.org/tr/2025.10/color/) | Colour values as objects in any CSS Color 4 space | | [Resolver](https://www.designtokens.org/tr/2025.10/resolver/) | Themes, brands and other contexts: sets, modifiers and the order in which they apply | ## Where DTA differs DTA's files are DTCG in the shape of Figma variables: one file per collection and mode (`Brand.BrandA.json`, `Brand.BrandB.json`), or a `.tokens` file for a collection with one mode. It reads every type of 2025.10, group `$type` and `$deprecated`, `$ref`, `$root` and `$extends`, and checks each value against its type; the [Token files](/reference/token-files/) reference has the format. Compared with 2025.10, DTA does **not** yet follow `$extends` from one per-mode file into another (it works within a file, and across the sets of a resolver document). This gap is tracked in the [DTCG 2025.10 spec](https://github.com/Fuasmattn/design-token-authority/blob/main/docs/specs/dtcg-2025-10.md) and its tickets. A minimal file DTA reads, with all four types and an alias, is [`learn-examples/dtcg-basics/`](https://github.com/Fuasmattn/design-token-authority/tree/main/site/learn-examples/dtcg-basics/). Next: [Token patterns](/learn/patterns/). --- # Token patterns URL: https://dta.martinprinz.dev/learn/patterns/ Each pattern below links to a small example project in the repository. A test runs `dta lint` and `dta build` on each one, so the examples stay correct as DTA changes. Open a folder to see the token files and config. | Pattern | The idea | In DTA | Example | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | **Tiers** | Primitives hold raw values; semantic tokens name roles (`action.background`); component tokens name parts (`button.background`). Each tier aliases the one below. | One collection per tier; `semantic-must-alias` keeps the upper tiers free of raw values | [`tiers`](https://github.com/Fuasmattn/design-token-authority/tree/main/site/learn-examples/tiers/) | | **Multiple brands** | One shared palette; the semantic tier has the same names in every brand, with different aliases. | A mode per brand, listed in `brands`; one `themes/.css` each | [`multi-brand`](https://github.com/Fuasmattn/design-token-authority/tree/main/site/learn-examples/multi-brand/) | | **Light and dark** | The same mechanism, with role names for surfaces and text. | Modes `Light` and `Dark`, listed in `brands` | [`themes`](https://github.com/Fuasmattn/design-token-authority/tree/main/site/learn-examples/themes/) | | **Breakpoint overrides** | Sizes that change with the screen. | A mode per breakpoint; read, linted and diffed, but not built yet (TICKET-091) | [`responsive`](https://github.com/Fuasmattn/design-token-authority/tree/main/site/learn-examples/responsive/) | | **Component overrides** | One brand changes one component without touching the semantic tier. | A component collection with the brand modes; the brand points one token elsewhere | [`component-overrides`](https://github.com/Fuasmattn/design-token-authority/tree/main/site/learn-examples/component-overrides/) | Brands and themes together (brand × theme) need one mode per combination for now; see [Brands and themes](/guides/brands-and-themes/). :::note[A component tier with several brands] A component tier with one mode works with brands: its tokens that alias brand tokens are repeated in each theme file, so a nested `data-brand` section changes them too ([Brands and themes](/guides/brands-and-themes/#tokens-that-alias-brand-tokens)). Give the component tier the brand modes when a brand should point a component token somewhere else, as in the `component-overrides` example. ::: ## How large design systems do it The tier names differ, but the structure is the same. Their own documentation is the source for the details: | System | Tiers, from raw values up | Source | | -------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | Material Design 3 | reference → system → component | [Material Web: theming](https://material-web.dev/theming/material-theming/) | | GitHub Primer | base → functional → component/pattern | [Primer: token names](https://primer.style/product/primitives/token-names/), [color](https://primer.style/foundations/color/overview) | | Adobe Spectrum | primitive (global) → semantic → component-specific | [Spectrum: design tokens](https://spectrum.adobe.com/page/design-tokens) | | Atlassian | palette → tokens named foundation.property.modifier | [Atlassian: design tokens](https://atlassian.design/foundations/tokens/design-tokens/), [color](https://www.atlassian.design/foundations/color) | | Salesforce Lightning | global styling hooks → component styling hooks | [Salesforce: styling hooks](https://developer.salesforce.com/docs/platform/lwc/guide/create-components-css-custom-properties) | What they share: code uses the decision tier, not raw values, and themes change the decision tier while the palette stays the same. --- # Guides URL: https://dta.martinprinz.dev/guides/ | Guide | For | | --------------------------------------------------------------- | ---------------------------------------------------------------- | | [Getting tokens out of Figma](/guides/figma/) | Every Figma plan: the REST API, plugin exports, writing files | | [Brands and themes](/guides/brands-and-themes/) | One token set, several brands or light and dark | | [Tailwind](/guides/tailwind/) | Using the generated Tailwind v4 (and v3) files | | [CI with GitHub Actions](/guides/ci/) | Lint and build on every push | | [dta diff in pull requests](/guides/dta-diff-in-pull-requests/) | Status check and comment with screenshots explained by token | | [dta diff with Storybook](/guides/dta-diff-with-storybook/) | Screenshot every story of a Storybook build | | [DTA with coding agents](/guides/coding-agents/) | The skill, the project context, token queries and the code audit | | [Migrating from v1](/guides/migrate-from-v1/) | What changes when you upgrade | --- # Getting tokens out of Figma URL: https://dta.martinprinz.dev/guides/figma/ How DTA reads your variables depends on your Figma plan. `dta init` asks which plan you have and sets `figma.source` to match. | Plan | Pull | Push | | -------------------------- | ------------------------------- | ----------------------------------- | | Enterprise | `dta pull` (Variables REST API) | `dta push` | | Professional, Organization | `dta pull --from-file ` | not available (the API returns 403) | ## Enterprise: the REST API `dta pull` reads the local variables of one Figma file. It needs the file key (from the file's URL, `figma.com/file//…`) and a personal access token (Figma › Settings › Personal access tokens). `dta init` writes both into `.env`, and the config reads them from there: ```ts title="dta.config.ts" export default { figma: { source: 'api', fileKey: process.env.FIGMA_FILE_KEY!, personalAccessToken: process.env.FIGMA_PERSONAL_ACCESS_TOKEN!, }, // … } ``` ```text title=".env (never commit it)" FIGMA_FILE_KEY=… FIGMA_PERSONAL_ACCESS_TOKEN=… ``` `dta` loads `.env` from the folder you run it in. A run on the `brand-ab` fixture against a recording of the Figma API, writing into `pulled/` instead of `tokens/`: ```text $ dta pull -o pulled T ⤵ dta pull | o Received 6 token files from Figma | o Wrote Primitives.Value.json | o Wrote Brand(Alias).BrandA.json | o Wrote Brand(Alias).BrandB.json | o Wrote ScreenType.Desktop.json | o Wrote ScreenType.Tablet.json | o Wrote ScreenType.Mobile.json | — Tokens written to pulled/ ``` `dta analyze` reads the same file and shows which collection plays which role, before you commit to a config. ## Any plan: a plugin export Export your variables with a Figma plugin, then import the file: ```sh dta pull --from-file exports/variables.json ``` DTA reads its own per-mode format directly ([Token files](/reference/token-files/)) and converts a few known single-file exports. On the repository's `daisyui` fixture, a public daisyUI library exported with the tokenHaus plugin: ```text $ dta pull --from-file ../../../docs/token-files/design-tokens-daisyui.json T ⤵ dta pull | o Converted tokenhaus export into 41 token files | o Wrote join-direction.Bottom.json … ``` DTA does not depend on any plugin. If yours writes something DTA does not know, convert it to the per-mode format (the JSON Schema helps) or [contribute an adapter](/contribute/). :::tip[Default mode names] Figma names the first mode of a new collection `Mode 1`. `dta lint` fails on that name (`no-default-mode-names`): rename the modes in Figma to what they mean (`Light`, `BrandA`, `Desktop`). ::: Figma has announced native variable export in the DTCG format; once it is generally available, `--from-file` is meant to read it directly. ## Pushing back to Figma `dta push` writes your token files to Figma (Enterprise only). It is conservative: it creates and updates variables and never deletes one. Before it changes anything it runs the lint check, shows the plan and asks you to type `push variables to figma`. ```text $ dta push --dry-run T ⤴ dta push | ! 8 lint warnings found. | o Diff computed | | Summary: +1 added, ~2 modified | | Added (1): | + Dimensions/radius/pill [VariableCollectionId:1:1] | | Modified (2): | ~ VariableID:1:1 (1:0) → {"r":0,"g":0.3411764705882353,"b":0.7215686274509804} | ~ VariableID:1:11 (1:0) → 8 | | — Dry run — no changes were applied to Figma. ``` `--yes` (or `push: { skipConfirmation: true }` in the config) skips the typed confirmation for CI. `--format markdown` or `json` prints the plan for a pull-request comment. ### Removing variables: dta prune Since `dta push` never deletes, a token you remove from the files stays in Figma as a variable without a token. `dta prune` (Enterprise only) finds those and deletes them, as a separate step you run on purpose: ```text $ dta prune --dry-run T ✂ dta prune | o Compared | o 1 variable in Figma without a token --------+ | | | Primitives Colors.Foundation.Old #3355d4 | | | +----------------------------------------------+ | — Dry run: nothing was deleted. ``` Without `--dry-run` it asks you to type `delete variables from figma`; `--force` deletes without asking (CI, on purpose). It is narrow by design: - only collections that are in your token files, with at least one token, are looked at; a Figma collection you never pulled, that `collections` leaves out, or whose file is empty is left alone, with a note; - a collection that would lose more than half of its variables is left alone too (that usually means the files are incomplete); `--allow-bulk` deletes there anyway; - an entry the files have but DTA does not read as a token (a `$` name, a value that is neither a token nor a group) stops prune: its variable would look orphaned; - library variables, and variables Figma has already deleted but still references, are never deleted; - a variable that other variables in Figma still alias is kept and named: run `dta push` first, then prune; - with no token files it stops instead of treating every variable as an orphan. Collection names are matched exactly: with `tokens.stripEmojis`, a Figma collection whose name has an emoji has no match in the files and is left alone. Figma's REST API does not say which components or frames use a variable, so prune cannot check that: layers that use a deleted variable lose its value. Look at the list before you confirm. ### What Figma variables can hold Figma variables hold colours, numbers, text and booleans. Of the DTCG 2025.10 types, `dta push` sends: - **colours** in any colour space, converted to sRGB; one outside sRGB is clipped, with a `clipped-to-srgb` warning naming the colour Figma gets; - **dimensions in `px`** as numbers; - **a font family** that names one font, as text; - **a `$ref`** to a token as an alias, and one into a value as that value. Everything else is skipped with a `not-pushable` warning that says why: `rem` dimensions, lists of font families, font weights, durations, cubic Béziers and the composite types (shadow, border, transition, typography, gradient, stroke style). An alias to a skipped token is skipped too, so Figma never gets an alias to a variable it does not have. `dta pull` writes what Figma holds, so those tokens stay in your files only. --- # Brands and themes URL: https://dta.martinprinz.dev/guides/brands-and-themes/ A brand (or a theme) is a **mode** of the collection that holds your semantic tokens. In the `brand-ab` fixture that collection is `Brand(Alias)` with the modes `BrandA` and `BrandB`; both alias into the same primitives: ```json title="tokens/Brand(Alias).BrandA.json (excerpt)" { "Colors": { "Brand": { "Primary": { "$type": "color", "$value": "{Colors.Foundation.Blue.500}" } } } } ``` ```json title="tokens/Brand(Alias).BrandB.json (excerpt)" { "Colors": { "Brand": { "Primary": { "$type": "color", "$value": "{Colors.Foundation.Navy.500}" } } } } ``` ## Build one theme per brand List the modes to build in `brands`: ```ts title="dta.config.ts" export default { brands: ['BrandA', 'BrandB'], // … } ``` `dta build` then writes the primitives once (`output/css/variables.css`, on `:root`) and one file per brand, scoped to an attribute with the lower-cased brand name: ```css title="output/css/themes/brandb.css (excerpt)" [data-brand="brandb"] { --colors-brand-primary: var(--colors-foundation-navy-500); … } ``` Load all of them and switch with the attribute. It works on any element, so a page can show two brands side by side: ```html …
this part uses BrandB
``` Because brand values stay `var()` references to the primitives, a change to `Colors.Foundation.Blue.500` reaches every brand that uses it without a rebuild of the themes. ### Tokens that alias brand tokens A collection with one mode (a component tier, say) is written to `variables.css` on `:root`. When one of its tokens aliases a brand token, directly or through other single-mode tokens, each theme file declares it again with the same reference. CSS resolves `var()` where a property is declared, so the copy in the theme is what lets a nested `data-brand` section resolve it to its own brand. A build of the Learn `multi-brand` example with a single-mode `Component` collection added (`button.background` → `{action.background}`, `button.radius` → `{shape.control}`, `card.accent` → `{button.background}`, `button.border` → `{color.blue.600}`): ```css title="output/css/themes/brandb.css" [data-brand="brandb"] { --action-background: var(--color-green-600); --action-foreground: var(--color-neutral-0); --shape-control: var(--radius-999); --type-heading: var(--font-serif); --button-background: var(--action-background); --button-radius: var(--shape-control); --card-accent: var(--button-background); } ``` All four are in `variables.css` on `:root` as before; `--button-border` aliases a primitive only, so it is not repeated in the themes. What `base.css` writes for them: [Outputs](/reference/outputs/#single-mode-tokens-that-alias-brand-tokens). ### Breakpoint tokens and brands The same holds for a [breakpoint collection](/reference/outputs/#breakpoint-modes): a breakpoint token that aliases a brand token in any of its modes is repeated in every theme, once per mode, inside the same media queries. The Learn `multi-brand` example with a `Screen` collection added (`card.radius` → `{radius.4}` on Mobile, `{radius.999}` on Desktop; `card.accent` → `{color.blue.600}` on Mobile, `{action.background}` on Desktop) and `breakpoints: { collection: 'Screen', default: 'Mobile', modes: { Desktop: '(min-width: 1024px)' } }`: ```css title="output/css/themes/brandb.css" [data-brand="brandb"] { --action-background: var(--color-green-600); --action-foreground: var(--color-neutral-0); --shape-control: var(--radius-999); --type-heading: var(--font-serif); } /* Screen · Mobile (default) */ [data-brand="brandb"] { --card-accent: var(--color-blue-600); } /* Screen · Desktop */ @media (min-width: 1024px) { [data-brand="brandb"] { --card-accent: var(--action-background); } } ``` So a `
` inside a BrandA page gets BrandB's green accent from 1024 px and the blue one below. `card.radius` reaches primitives only and stays in `variables.css`. ## Light and dark Light and dark are the same mechanism: a collection with the modes `Light` and `Dark` (or `Graphite`), listed in `brands`. This website does exactly that: its tokens have a `Theme` collection with `Light` and `Graphite`, built into `[data-brand="light"]` and `[data-brand="graphite"]`, and a small script sets the attribute when you switch the theme. ## Brands and themes together Per-mode files hold one dimension per collection, so a set with both brands and light/dark (BrandA light, BrandA dark, …) needs one mode per combination there. A DTCG [resolver document](/reference/token-files/#resolver-documents) describes them as two **modifiers** instead, and `dta build` combines them. In the `dtcg-modifiers` fixture a `theme` (light, dark) and then a `brand` (a, b) both change `accent`: the sets give it blue, the dark theme a lighter blue, brand b navy (it comes later in the resolution order, so it wins). ```ts title="dta.config.ts" export default { brands: ['a', 'b'], // the brand modifier switches with data-brand; the theme with data-theme // … } ``` Tokens one modifier changes go to that modifier's theme files as above (`themes/theme-dark.css` with `[data-theme="dark"]`). A token both change gets a custom property per layer, and reads the last one that is set: ```css title="output/css/variables.css (end)" /* theme × brand: the sets' values and the default contexts; [data-theme] and [data-brand] choose others */ :root { --accent-dta-base: var(--color-blue-500); --accent: var(--accent-dta-brand, var(--accent-dta-theme, var(--accent-dta-base))); } ``` ```css title="output/css/themes/theme-dark.css (end)" /* theme × brand: theme · dark */ [data-theme="dark"] { --accent-dta-theme: var(--color-blue-300); --accent: var(--accent-dta-brand, var(--accent-dta-theme, var(--accent-dta-base))); --button-background: var(--accent); } ``` ```css title="output/css/themes/b.css (end)" /* theme × brand: brand · b */ [data-brand="b"] { --accent-dta-brand: var(--color-navy-700); --accent: var(--accent-dta-brand, var(--accent-dta-theme, var(--accent-dta-base))); --button-background: var(--accent); } ``` `:root` also holds the layers of each modifier's default context (none here: light and a leave `accent` alone), so a page without the attributes gets the default combination. A context that leaves the token alone sets its layer to `initial` (brand a, light), so a nested section can undo an outer one. Set both attributes on any elements, in either order: ```html
dark × a
dark × b
``` Each combination gives the value the resolver document gives (a browser test checks all four, nested both ways). `dta token get accent` lists them, and `color-contrast` checks a pair in each combination. Two things to know: - Single-mode tokens that alias such a token (`button.background` → `{accent}`) are declared again in every context, like [tokens that alias brand tokens](#tokens-that-alias-brand-tokens). - A token of one modifier that aliases it (`ring` → `{accent}` in the dark theme) is declared again inside each of that modifier's contexts, wherever an element sets the other modifier, with the single-mode tokens that alias it (`button.focus` → `{ring}`). `@scope` lets the nearest context win (a browser without `@scope` ignores the block, and such a nested section keeps the outer value): ```css title="output/css/themes/theme-dark.css (end)" /* theme × brand: theme · dark tokens again where [data-brand] is set inside */ @scope ([data-theme="dark"]) { :scope[data-brand], [data-brand] { --ring: var(--accent); /** Focus ring */ --button-focus: var(--ring); } } ``` - Tailwind and JS outputs leave combined tokens out; use the CSS custom properties. A token that aliases one is written resolved there for the defaults of the other modifiers (the Tailwind file of the dark theme has `ring` for dark × a). ## Keep the brand layer clean The `semantic-must-alias` rule fails when a token in the brand layer holds a raw value instead of an alias, so a brand never drifts from the palette: ```ts lint: { rules: { 'semantic-must-alias': { severity: 'error', collections: ['Brand(Alias)'] } } } ``` `color-contrast` checks text and background pairs through their aliases, once per brand (and per light or dark mode, if you have them); a finding names the brand it fails in: [Lint rules](/reference/lint-rules/). --- # Tailwind URL: https://dta.martinprinz.dev/guides/tailwind/ `dta build` writes Tailwind files next to the CSS: | File | For | | -------------------------------------------- | ------------------------------------------------------------------------ | | `output/tailwind/tailwind.css` | Tailwind v4: an `@theme` block of the primitives (as `var()` references) | | `output/tailwind//tailwind.css` | Tailwind v4: one brand's tokens, with resolved values | | `output/tailwind/tailwind.tokens.ts` | Tailwind v3: a `theme.extend` object | | `output/tailwind//tailwind.tokens.ts` | Tailwind v3: one brand, resolved | ## Tailwind v4 Import the token CSS and the `@theme` file into your stylesheet. The `@theme` entries point at the custom properties, so `variables.css` must be loaded too: ```css title="src/app.css" @import 'tailwindcss'; @import '../output/css/variables.css'; @import '../output/css/themes/branda.css'; @import '../output/css/themes/brandb.css'; @import '../output/tailwind/tailwind.css'; /* Brand tokens switch with [data-brand]: map them as theme variables that stay references. */ @theme inline { --color-brand-primary: var(--colors-brand-primary); --color-brand-accent: var(--colors-brand-accent); } ``` Then use the utilities: ```html ``` Compiled with Tailwind 4.3.3 against the `brand-ab` fixture, these utilities come out as: ```css .rounded-dimensions-radius-md { border-radius: var(--radius-dimensions-radius-md); } .bg-brand-primary { background-color: var(--colors-brand-primary); } .p-4 { padding: var(--spacing-4); } .text-foundation-white { color: var(--color-foundation-white); } ``` Two things to know: - **Spacing replaces Tailwind's scale.** The fixture's `Dimensions.spacing.4` becomes `--spacing-4` (16px), so `p-4` uses your token instead of Tailwind's default calculation. - **Breakpoint tokens switch too.** With [`breakpoints`](/reference/outputs/#breakpoint-modes) in the config, `tailwind.css` ends with an `@theme inline` block for the breakpoint collection: `p-spacing-gap` compiles to `padding: var(--spacing-gap)`, which `variables.css` sets per media query. Load `variables.css`, as above. - **Brand files are resolved.** `output/tailwind//tailwind.css` holds one brand's final values: use it when a build ships one brand only. For switching brands at runtime, map the brand tokens with `@theme inline` as above, so the utilities keep reading the custom properties. ## Tailwind v3 Spread the generated object into your config: ```ts title="tailwind.config.ts" export default { content: ['./src/**/*.{html,tsx}'], ...tokens, } satisfies Config ``` Its values are `var()` references too (`'foundation-blue-500': 'var(--colors-foundation-blue-500)'`), so load `output/css/variables.css` in your app. --- # CI with GitHub Actions URL: https://dta.martinprinz.dev/guides/ci/ The two commands that belong in every pipeline: - `dta lint` exits with **2** when it finds errors (dangling or circular aliases, default mode names, rules you set to `error`). Warnings print but exit with 0. - `dta build` exits with **1** when the token files cannot be read or built, among them a dangling or circular alias, and then writes nothing; otherwise it writes the outputs. All exit codes: [Exit codes](/reference/exit-codes/). ## Workflow `design-token-authority` is a dev dependency of your project (`npm create dta` adds it), so the workflow installs your dependencies and runs `npx design-token-authority` (Node.js 24 or newer). With pnpm or yarn, swap the install step and `npx` for `pnpm exec` or `yarn`. ```yaml title=".github/workflows/tokens.yml" name: tokens on: [push, pull_request] permissions: contents: read jobs: tokens: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 - run: npm ci - name: Lint tokens run: npx design-token-authority lint - name: Build outputs run: npx design-token-authority build ``` Commit the `tokens/` folder, not the build output: CI rebuilds it, and `dta clean` removes it locally. ## On pull requests - **What changed in the tokens:** `dta diff --tokens-only -f markdown -o token-changes.md` writes the change set as a Markdown table for a comment or the job summary (`>> $GITHUB_STEP_SUMMARY`). - **What it looks like:** with a `diff` section in the config (`dta init` writes it, and the workflow), `dta diff` renders your stories at the base and the head and explains the differences by token. [dta diff in pull requests](/guides/dta-diff-in-pull-requests/) has ready-made actions for the status check and the comment. - **Pushing to Figma from CI** (Enterprise): `dta push --yes` skips the typed confirmation. Run `dta push --dry-run --format markdown` on the pull request and the real push only on the main branch. The token diff needs the base commit: check out with `fetch-depth: 0`, or pass `--base `. ## Hard-coded values in code `dta audit` scans your source for colours, lengths and font families that equal a token and names the token to use (CSS, SCSS, JS/TS/JSX/TSX, the `