# NetScript Custom Design Guidelines How to design a Fresh UI theme that matches the quality of the reference themes and works with the NetScript framework. The guidelines have two readers: - a designer creating or extending a theme; - an AI agent working from a `SKILL.md` and `DESIGN.md` pair. The guidelines define the token contract every theme follows (§1 to §3), the method for designing and checking a theme (§4 and §5), the existing themes (§6), and the steps for adding and shipping a new one (§7 and §8). > **Status: token names are provisional.** The role names below are the current Figma contract. The > tokens build (#2414) turns them into DTCG 2025.10 tokens with resolver modes. The component > anatomy spec (#2416) freezes anatomy and data-attribute variants. Until both land, treat names as > stable and values as owned by Figma. Do not invent new core role names. ## 1. Two rules that override everything 1. **Figma owns values; code owns names.** A theme changes the values of existing roles, tiers and modes. It never renames a core role or introduces a new core role on its own. New namespaced roles come only through a style's role manifest (§2.4). CI rejects exported token names that code does not know. 2. **A theme is a language, not a palette.** Recolouring and re-rounding the default UI is not a theme. A theme language is a **style**: it owns a depth model, a shape language, a type system and its own version of every component, each traceable to a documented reference (§2.4). ## 2. Theme architecture Figma and code describe the same system. Figma stores values in variable collections and modes. In code, the tokens build (#2414) stores tokens in three DTCG 2025.10 tiers and selects between contexts with a DTCG resolver file. This section maps one onto the other. Ownership follows the epic (#2412, decision 2): Figma owns values; code owns names, tiers, modes and component anatomy. ### 2.1 Three token tiers | Tier | What it holds | Figma source | Code surface (provisional until #2414) | Who may read it | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------- | | **Primitive** | Raw values only: colour ramps (oklch with a hex fallback), dimensions, durations, font families. Radix 12-step ramps (gray, slate, sand, mauve, blue, violet, orange, indigo, tomato, lime, grass, amber, red, teal) and theme ramps (`ember`, `band`, `paper`, `glass`, `shadow`, `swiss-red`) | `Primitives` collection (Value mode) | Internal custom properties, e.g. `--ns-band-*` | The semantic tier only. Components never read primitives | | **Semantic** | **Aliases only.** Every value references a primitive (or another semantic role), never a raw hex, px or ms. Core roles plus each style's namespaced roles (§2.4) | `Scheme` collection (Light, Dark). Its aliases are selected through the `Theme set` (`Theme` or `Theme B`), `Base gray`, `Base accent`, `Density` and `Studio flavor` modes (§2.2) | `--ns-*` (core) and `--ns--*` (styles) | Component tokens and L2 component CSS | | **Component** | Part and state tokens (`button.*`, `input.*`, `card.*`, …) that alias semantic roles, so heavy customization is a token edit, not a CSS override | Variable bindings on library components; there is no component collection yet | Named by the tokens build from the component anatomy spec (#2416) | That component's own CSS only | - **Derived values.** Tints, hovers and other mixes are not raw values in the semantic tier. `$extensions.netscript.derive` describes them. CSS keeps `color-mix()`, and Figma receives the computed value. - **Collections and modes are not tiers.** `Scheme`, `Theme`, `Theme B`, `Base gray`, `Base accent`, `Density` and `Studio flavor` modes are contexts the resolver chooses between (§2.2). `Theme set` only switches the aliases between `Theme` and `Theme B`. None of them adds a fourth tier. - **Raw values inside Figma modes.** Some Theme modes still store raw values in slots that are semantically aliases, for example Tactile's `surface/{l,d}/*` and Fluent's `accent/{l,d}/solid`. Each theme's `DESIGN.md` records them. Before export, the tokens build (#2414) splits each one into a primitive and an alias. - **Node-level exceptions are Figma-only.** A primitive or raw value bound directly on a Figma node is a temporary exception. The theme's `DESIGN.md` names the role that replaces it, pending in #2414. It never ships in L2 component CSS. - **Layer ownership is unchanged.** L0/L1 own platform behaviour, attributes and prop getters. L2 registry components consume component and semantic tokens. An L2 component never imports another L2 component. ### 2.2 How Figma collections map to the resolver | Figma collection (modes) | What it selects | Token resolver (#2414) | Root attribute (provisional) | | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | | `Scheme` (Light, Dark) | The semantic role values for each scheme | `scheme` modifier: `light`, `dark` | `data-scheme` | | `Theme` (Studio, Ledger, Filigrane, Signal, Tonal, Glass, Brutal, Fluent, Tactile, Pixel) | Language values: fonts, type scale, radii, materials, shadow recipe, ring | `theme` modifier, with one context per **language preset** (§2.3), not one per Figma mode | `data-theme` | | `Theme B` (Bloom, Cupertino, Liquid, Terminal, macOS 27, Apple Classic, Fluent 2 Web) | Language values, as for `Theme` | The same `theme` modifier | `data-theme` | | `Theme set` (A, B) | Which Theme collection the semantic roles alias through: A selects `Theme`, B selects `Theme B` | Not a modifier; the `theme` modifier covers both Theme collections | — | | `Density` (Comfortable, Compact, Dense, Airy, Chunky) | Control, page, section, card, table and badge spacing | `density` modifier. The token resolver specifies `comfortable` and `compact` only; `dense`, `airy` and `chunky` need a resolver schema change | `data-density` | | `Studio flavor` (Utility, Cloud, Workshop, Playful, Ledger, Atelier, Signal) | Personality on top of the Studio language | Not a modifier. Each flavor expands into a Studio preset | `data-flavor` (Studio only) | | `Base gray` (Neutral, Slate, Sand, Mauve) | Which gray ramp a theme sits on | Not a modifier. A preset parameter | `data-gray` | | `Base accent` (Ink, Blue, Violet, Orange, Indigo, Tomato, Lime) | Which accent ramp drives `primary/*` | Not a modifier. A preset parameter | `data-accent` | | `Primitives` (Value) | Raw ramps | The primitive tier; no modifier | — | This Figma plan allows **10 modes per collection**, and `Theme` uses all 10. A second collection, `Theme B`, holds the modes for Bloom, Cupertino, Liquid and Terminal, and for the planned macOS 27, Apple Classic and Fluent 2 Web themes (#2443). The semantic roles alias through the `Theme set` collection: set A selects `Theme` and set B selects `Theme B`. Each language has its own mode. The `Theme B` modes were seeded with copies of existing values: Bloom from Tonal; Cupertino, Liquid, macOS 27 and Apple Classic from Glass; Terminal from Signal; and Fluent 2 Web from Fluent. Each mode can diverge. Until a language's own values are set, its mode resolves to its seed values, which are prototype evidence only. Before a language ships, its values must come from its own mode, and the token export (#2414) must give each language preset its own complete core-role map. When both collections are full, a new language replaces a mode or needs a new Theme collection. The token resolver (#2414) has no mode limit in code. ### 2.3 Supported resolver axes The axes are scheme × theme × flavor × density × gray × accent. **Not every permutation exists.** The unit of support is the **language preset**: a fixed choice of Theme values, density, gray and accent (and, for Studio, flavor), designed and reviewed in both schemes. | Axis | Values | Where it applies | Supported today | | --------- | ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | `scheme` | light, dark | Every preset | Both schemes for every preset; each dark is designed, not inverted | | `theme` | `studio`, `tonal`, `bloom`, `cupertino`, `liquid`, `fluent`, `brutal`, `filigrane`, `terminal`, `pixel`, `tactile` | — | One preset per language; Studio has seven, one per flavor | | `flavor` | utility, cloud, workshop, playful, ledger, atelier, signal | Studio only | Each flavor fixes its own gray, accent and density (Studio `DESIGN.md` → _Flavors_). Every other language ignores it | | `density` | comfortable, compact (token resolver); dense, airy, chunky (Figma only) | Studio flavors; a few languages set one explicitly | Fixed per preset; not a free user override yet | | `gray` | neutral, slate, sand, mauve | Studio flavors | Fixed per preset. The other languages alias their own ramps inside their Theme mode and ignore it | | `accent` | ink, blue, violet, orange, indigo, tomato, lime | Studio flavors | Fixed per preset, as for gray | **Precedence.** The resolver applies `theme` (with `flavor` for Studio) as a preset that fixes density, gray and accent. `scheme` then selects the Light or Dark values. An app's override layer (#2414) applies last, on the semantic tier only. A preset ignores attempts to change an axis it fixes, and the builder (§8) offers only supported combinations. A free axis, such as any density on any language, needs a resolver schema change (#2414) and a contrast run for every new permutation. **Fixed axes per language.** This table comes from each theme's `DESIGN.md` → _NetScript Mapping_. The mode assignments are read from the Figma library and are not yet verified in code (#2414). Modes in set B live in `Theme B` and still hold their seed values (§2.2). | Language | Figma Theme mode | Density | Gray × accent | | --------- | ---------------------------------------- | ----------------------------------------- | ------------------------------------------------------- | | Studio | `Studio` | per flavor (Utility: compact) | per flavor (Utility: neutral × ink) | | Tonal | `Tonal` | comfortable; M3 component sizes are fixed | mauve × violet, inside the mode | | Bloom | `Bloom` (set B, seeded from `Tonal`) | none (literal geometry) | none; Bloom reads `bloom/*` | | Cupertino | `Cupertino` (set B, seeded from `Glass`) | comfortable | slate × blue, inside the mode | | Liquid | `Liquid` (set B, seeded from `Glass`) | comfortable | slate × blue, inside the mode; own values in `liquid/*` | | Fluent | `Fluent` | none (WinUI defaults) | neutral gray × WinUI accent, inside the mode | | Brutal | `Brutal` | none (comfortable default) | sand × amber, inside the mode | | Filigrane | `Filigrane` | none | sand × tomato, inside the mode | | Terminal | `Terminal` (set B, seeded from `Signal`) | compact | gray × lime, inside the mode | | Pixel | `Pixel` | comfortable | sand-family × amber, inside the mode | | Tactile | `Tactile` | none | none; Tactile reads `tactile/*` | **Migration from today's selectors.** Code today (NS One) scopes light on `:root` and dark on `[data-theme='dark']`. The new model moves the scheme to `data-scheme` and gives `data-theme` to the language: `data-theme="dark"` becomes `data-theme="" data-scheme="dark"`. Acceptance for the tokens build (#2414) requires that existing `--ns-*` names keep resolving or have a documented migration, and that NS One renders unchanged. The tokens build therefore either keeps `[data-theme='dark']` resolving as the default preset's dark scheme or ships that migration, with a `ui:migrate` step for app-owned copies. Never rename an existing `--ns-*` name without a migration. ### 2.4 Styles and token-only presets There are two kinds of theme. Keep them apart. - **Token-only preset.** Assigns values to existing roles: a Studio flavor, a gray or accent choice, an app's brand theme. It adds no names and no component sources. Most app themes are this kind. - **Style.** Every theme language (Studio, Tonal, Bloom, Cupertino, Liquid, Fluent, Brutal, Filigrane, Terminal, Pixel, Tactile) is a style, in the sense shadcn uses the word: it restyles **every** component, including shape, borders, fills, shadows, decorations and inner markup, over one shared contract. A Pixel switch is Pixel's own switch, not the base switch with a smaller radius. - **The contract does not change between styles** (#2416): each component keeps its props, `data-part` names, `data-variant`, `data-size` and `data-state` values, slots, APG pattern and keyboard map. The contract tests run against every style. - **Tokens stay separate axes** under every style: scheme, gray, accent, radius and density. - A style declares: 1. **Its component sources:** one per component, shipped as a registry `style` item (#2415). 2. **A role manifest:** every namespaced role it adds, as `/` → `--ns--`, each with a Light and a Dark alias. Until the tokens build has a machine-readable manifest, the list is the _Language roles_ row in the theme's `DESIGN.md` → _NetScript Mapping_. The tokens build registers every listed role before the style ships; unknown names fail CI. 3. **A complete core map:** every core semantic role still resolves under the style, so token-only presets and app-owned components keep working. - A style restyles its own components only, never app-owned markup outside them. - **In Figma,** each style is a set of component sets named `