---
title: "Theme Studio"
description: "Move a few controls, watch real components follow, and export a valid design-system layer you can build for every platform."
url: "https://tanrei.chnotchy.com/foundations/theme-studio/"
last_updated: 2026-10-26T22:31:37.000Z
---

# Theme Studio

> Move a few controls, watch real components follow, and export a valid design-system layer you can build for every platform.

A design system should feel right by default and follow you when you change one thing. The Studio is that idea, made touchable: pick a base, turn a dial, choose a color, change one token, add one of your own — and export the small [layer file](https://tanrei.chnotchy.com/foundations/design-systems/#a-design-system-is-a-layer) that says exactly what you changed, nothing else.

- [Open Theme Studio](https://tanrei.chnotchy.com/studio/): The Studio on a page of its own, with the whole window for the preview.

Nothing in it is a mock-up. The preview is a real system scope with real components, and it applies the very stylesheet `tanrei build` would write for your layer — computed in your browser by the same engine, emitter and accessibility checks. Repository tests build Studio layers with the CLI pipeline and require the whole sheet, every token value in all six schemes and every warning to match.

It shows your theme on five screens built from those components — an overview of every kind of component, a dashboard, a settings form, a list and its detail, and a documentation page — in light, in dark, or both side by side, at each of the three contrast levels. Which screen you look at, and how, is not part of your theme: none of it is saved in the file.

The layer you are making stays in this browser, and in the page’s address: a link to the Studio opens it on the same layer. A link never writes over the layer a browser kept: a banner over the Studio says which layer came with the link and what it is built on, and the Studio keeps yours until you edit the link’s or choose to keep it — or go back to yours.

The Studio’s bar starts with **where your layer starts**: Tanrei, the default system, or an example of the theme gallery — m3-like, cupertino and m3-expressive-like, each a showcase of what a layer can imitate, not that system’s official design. Core, Tanrei’s root layer, is listed and cannot be chosen yet: it still holds the default system’s values, and becomes a blank start once they are neutral. The same menu imports a layer file in place of the one you are making. A file the schema refuses is not loaded, and the panel says why; a file it accepts is loaded, and the panel lists what the token engine finds in it. When your layer holds changes, the Studio asks before it replaces them, and **Undo** brings them back.

**Export**, in the Studio’s bar, opens a side sheet with the file and what to do with it. Its first line says whether the schema, the token engine and the build’s checks accept the file. A layer started from an example of the theme gallery depends on that example by default: the file stays small and follows the example as it improves. You can also write every value out, so that the file no longer changes when the example does; a component that lives in the example is still taken from it. Then come the file itself, to copy or download; the declarations of the stylesheet `tanrei build` writes for the web that differ from the base system’s, with the whole stylesheet to download; and the next steps, each a command the CLI takes as it is, with a link to share the layer.

On the left, the rail lists the nine parts of a look — brand color, gray, typeface, size, corners, corner smoothing, icons, press feedback and focus — each with what it holds now and a small sample, then the rest of what a layer holds. A row opens its controls in the panel on the right. Lock the rows you like, and **Shuffle** gives every other row another value, in one step to undo. **Inspect** opens every token, and the layer file itself, over the panel. Every change can be undone and redone, with the buttons in the Studio’s bar or with ⌘Z and ⇧⌘Z (Ctrl+Z and Ctrl+Shift+Z).

## What the controls write

| You do | The layer gets |
| --- | --- |
| Pick a base system | `"extends"` |
| Move a dial | `"$value"` on `dial.*`, with a `reason` |
| Choose a seed or hue + chroma, declare or drop a palette | an entry in `"palettes"` — `null` drops an inherited one |
| Fix a tone of a palette to an exact color | `source.tones` of that palette — `{ "40": "#e60012" }`; a new row starts as the generated color |
| Order the families of **Brand**, **Plain** or **Mono**, choose the generic fallback, put the platform’s font first | `{ "$value": ["IBM Plex Sans", "system-ui", "sans-serif"], "reason": "…" }` on `ref.typeface.*` |
| Choose the icon set — one variable family, or a family per selection state — its weight and fills | `$value` pins on `ref.typeface.icon` or `sys.icon.family.*`, `sys.icon.weight`, `sys.icon.fill.*` |
| Move the corner-smoothing slider | `{ "$value": 0.6, "reason": "…" }` on `sys.shape.corner-smoothing` — an amount 0–1 with Figma’s meaning ([Shape](https://tanrei.chnotchy.com/foundations/shape/#corner-smoothing)) |
| Choose how a press is answered, how focus looks | `{ "$value": 1, "reason": "…" }` on `sys.behavior.*` — you pick a **name** (`ripple`, `glow`); the file stores its position |
| Scale a token | `{ "scale": n }` — a ratio of what it inherits, still driven by its dial |
| Re-point a token | `{ "$value": "{another.token}" }` |
| Pin a token | `{ "$value": …, "reason": "…" }` — exempt from every dial; not written until it has a reason |
| Replace a rule | `{ "derive": … }` — a coefficient on a dial, a tone table for a color role, `mix` or `alpha` for a state |
| Edit pattern rules | `"rules"` — how hovered, focused, pressed and disabled look, for every component at once |
| Add… a size or a variant, a step in a series, a token of your own | a declaration under `"vocabulary"`, with a mandatory `rationale` |

Typefaces, Icons, Shape and Behaviour write pins, and a pin needs a reason: each of the four asks for it once, prefilled (“Brand typeface.”), and every pin it writes carries your wording. In **Inspect**’s token tree a choice of kind is picked by name, never typed as a number, and a font stack is the same ordered list as in the **Typeface** row. The buttons in the preview are real, so `ripple`, `dim`, the eased corners and the focus glow are what `<tanrei-button>` really does with those tokens in this browser. The type ramp draws a heading of each face, running text, a label and code — and every text style your layer adds — and the **Emphasized** toggle on the preview bar redraws them at their role’s weight plus the family’s `sys.typescale.emphasis.*` step, live from the sheet. When a choice would draw nothing — `dim` while every pressed opacity is 1, as under the default system — the build’s own warning is shown under the control, word for word, and the token it names is a link to its editor.

A control left at its base value writes nothing, so an untouched Studio exports `{ "id", "extends" }` and builds to the base system exactly. Anything a valid layer can contain is editable here — importing a layer keeps nothing on the side.

## On your own file

`tanrei studio` serves this Studio locally and points it at your product’s layer file: edits are saved to the file as you make them, **Build** runs `tanrei build`, and when the file changes underneath — an editor, a `git checkout` — the Studio offers to reload or to keep what is open. It never overwrites silently. A save is written only when the schema, the rules for a chain of layers and the token engine all accept the layer — the server asks the engine itself and answers with its message — so the file on disk is always one `tanrei build` builds, whichever program sent it. The app also serves the typefaces and icon fonts that the systems shipped with Tanrei name (Inter, Noto Sans JP, Roboto Mono, Roboto, Material Symbols Rounded and Outlined) from its own address, so a preset is drawn in its own faces.

A product may stack its own layers — `acme-app.system.json` extending `acme.system.json` next to it. `tanrei studio` hands the layers next to your file to the Studio, read-only, and the Studio resolves the whole chain in the browser exactly as it does for the shipped systems; when a parent changes on disk, everything is computed again.

## What it cannot do

On the site there is no file behind the Studio, so a layer that extends a layer of your own cannot be imported here — open it with `tanrei studio`, which knows the files next to it. The Studio never fetches a font from anywhere but the site that serves it: a family that site does not provide and your browser does not have is named, and the preview honestly shows the fallback. Its “preview equals build” tests compare the web sheet, the token bundle and the warnings; the native outputs are generated from the same model by `tanrei build`.

The command line tool takes no layer in a command: `tanrei init` starts a layer of its own, and the file you download from Export takes its place.
