An inspector package is one JSON file, named <project>.inspector.json, that holds a project's design tokens and screens. Drop it on the Design Inspector, or choose it with Open.
The model has two layers. Primitives hold raw values: gray-580, space-8, radius-5, font-geneva. Semantic tokens name a role and point at a primitive, per theme: text-secondary is gray-580 in light and gray-230 in dark. Screens use semantic tokens only. The inspector reads which token a rule actually uses, so two tokens that share a value are never confused.
{
"format": "design-inspector",
"version": 2,
"project": "Dotfiles Classic",
"subtitle": "Dotfiles for Mac",
"code": "swiftui",
"fonts": ["https://fonts.googleapis.com/css2?family=Pixelify+Sans:wght@500;600;700&display=swap"],
"themes": [{ "id": "light", "name": "Light" }, { "id": "dark", "name": "Dark" }],
"primitives": { "color": [], "space": [], "radius": [], "font": [] },
"semantic": { "color": [], "space": [], "size": [], "radius": [], "effect": [] },
"textStyles": [],
"css": ".rx-btn { background: var(--background-control); … }",
"screens": [{ "id": "push", "name": "Push dialog", "width": 1280, "height": 820, "html": "<div class=\"rx rx-win\">…</div>" }],
"components": { "rx-def": "Button · default", "rx-btn": "Button" },
"states": { "rx-on": "selected" },
"patterns": { "rx-stripes": "Pinstripes" }
}
| Field | Required | What it is |
|---|---|---|
format, version |
yes | "design-inspector" and 2. |
project |
yes | Project name, shown in the top bar. subtitle is optional. |
themes |
yes | Theme ids and names, primary theme first. The top bar shows one button per theme. |
primitives |
yes | Raw values by family: color, space, radius, font. |
semantic |
yes | Role tokens by family: color, space, size, radius, effect. |
textStyles |
no | Semantic text styles. |
css |
no | CSS shared by every screen. |
screens |
yes | One or more screens. |
themeSwitch |
no | How the markup changes with the theme. Default { "apply": "vars" }. |
code |
no | "swiftui", "css" or "none" (default): the snippet the inspector shows. |
fonts |
no | Google Fonts stylesheet URLs. Other hosts are blocked. |
fontFaces |
no | Embedded fonts: [{ "family", "src": "data:font/woff2;base64,…", "weight", "style" }]. |
components |
no | CSS class or selector → component name. First match wins, so list specific entries first. |
states |
no | Class or selector → state word appended to the name, e.g. "Button · selected". |
patterns |
no | Class or selector → label for its background-image. |
Every token is { "name", "value", "usage" }. usage says where the token is used, in one sentence. Names are lower-case words joined by hyphens, and unique across primitives and semantic tokens. A value refers to another token as "{name}".
Primitives don't change with the theme.
| Family | Name | Example |
|---|---|---|
color |
{hue}-{step}; white, black; alpha {base}-a{percent} |
gray-580: #4d4d4d, black-a34: #00000057 |
space |
space-{px} |
space-8: 8px |
radius |
radius-{px}, radius-full |
radius-5: 5px, radius-full: 50% |
font |
font-{family} |
font-geneva: Geneva, "IBM Plex Sans", system-ui, sans-serif |
The color step is (1 − OKLCH lightness) × 1000, rounded to 10, so 0 is white and 1000 is black, and steps sort from light to dark. The hue word is the nearest of gray (chroma under 0.012), stone (warm gray), red, orange, amber, yellow, lime, green, teal, cyan, blue, indigo, violet, purple and pink.
Names follow role, then element, then variant, then state: {role}-{element}[-{variant}][-{state}].
| Family | Roles | Examples |
|---|---|---|
color |
background, text, icon, border, fill, shadow |
background-control-pressed, text-secondary, border-focus, fill-state-modified |
space |
padding, gap, margin, offset |
padding-button-x, gap-list-row, padding-list-row-leading |
size |
height, width, size (square) |
height-control, width-stroke, size-checkbox |
radius |
radius |
radius-button, radius-tab |
effect |
effect |
effect-raised, effect-frame-dialog |
Use the element words people use for the UI (button, row, tab, field), and x, y, top, bottom, leading or trailing for one side. Text that isn't tied to an element uses primary, secondary, placeholder or disabled.
A semantic value is an alias, either one for all themes or a map per theme:
{ "name": "text-secondary", "value": { "light": "{gray-580}", "dark": "{gray-230}" }, "usage": "Captions, paths, counts and section labels." }
{ "name": "padding-button-x", "value": "{space-12}", "usage": "Horizontal padding inside a standard button." }
{ "name": "effect-raised", "value": "inset 1px 1px 0 {border-bevel-highlight}, inset -1px -1px 0 {border-bevel-shadow}", "usage": "Raised bevel on buttons, panels and dialogs." }
size tokens may hold raw lengths, since sizes rarely have a scale. Elsewhere, a raw semantic value is allowed but listed on the Token check page as "no primitive".{ "name": "button-label", "font": "{font-pixelify-sans}", "size": "14px", "weight": 500, "lineHeight": "normal", "tracking": "0", "case": "none", "usage": "Standard buttons.", "sample": "Check Now" }
font refers to a font primitive. lineHeight is a length, a plain number or normal. tracking (letter spacing) and case (none, uppercase, lowercase or capitalize) are optional. Optional style is italic. Text style names describe the element and part: button-label, row-title, caption, section-label.
The inspector defines a CSS variable for every token, with the current theme's value, on the screen frame:
--{name} for every primitive and semantic token, for example var(--text-secondary).--type-{name} is a complete font value. --type-{name}-tracking and --type-{name}-case are also defined.--screen-w and --screen-h hold the frame size.Screen CSS and inline styles follow these rules. The inspector checks them on load and lists every exception on the Token check page.
background: var(--background-control). No hex, rgb() or named colors, and no color primitives.font: var(--type-button-label), plus letter-spacing: var(--type-button-label-tracking) and text-transform: var(--type-button-label-case) when the style sets them. Don't set font-size or font-weight on their own.padding: 0 var(--padding-button-x), height: var(--height-control), border-radius: var(--radius-button), border: var(--width-stroke) solid var(--border-control).gap: var(--space-8). Raw pixels are flagged.box-shadow: var(--effect-raised), var(--effect-ring-button-default).style="fill: var(--fill-folder)" or come through currentColor. Presentation attributes such as fill="…" can't hold variables.auto, transparent, currentColor, inherit and percentages in layout are fine.Screen CSS should not define its own variables for colors. Where it does, the inspector follows a local variable to the token behind it.
| Field | Required | What it is |
|---|---|---|
id |
yes | Unique and stable. |
name |
yes | Shown in the screen list and above the frame. |
width, height |
yes | Frame size in CSS pixels (points on Apple platforms). |
html |
yes | Static markup. The first element is the screen root. |
css |
no | CSS for this screen only. |
group |
no | Heading in the screen list. |
Scripts don't run, and clicks select elements. There is no html or body; :root is rewritten to :host. @font-face rules are moved to the page. Images must be data: URIs. data-inspector-name="…" names one element and wins over components.
Usually nothing in the markup changes, because the token variables carry the theme. When the CSS also needs a hook:
"themeSwitch": { "apply": "class", "values": { "light": "theme-light", "dark": "theme-dark" } }
"themeSwitch": { "apply": "attribute", "attribute": "data-theme", "values": { "light": "light", "dark": "dark" } }
For each property of the selected element, the inspector finds the declaration that wins the cascade: inline style, then matching rules by importance, specificity and order, then inheritance where the property inherits. It reads the variable that declaration uses and shows:
If the rendered value differs from the token's value, the inspector says so.
The inspector refuses a file with the wrong format or version, screens missing a required field, duplicate token names, aliases to tokens that don't exist, or alias loops, and says which. Smaller problems load anyway and appear under Package notes.