--- title: Eidos Theming — Authoring guides type: guide audience: human + agent status: current source: migrated from src/uix/eidos/THEMING_GUIDE.md (2026-07-02, docs-book F7.3; originally extracted from THEMING §8 + §9) --- # Eidos Theming — Authoring guides > The two theming how-tos: add a new component and define a theme. Extracted > from [`THEMING.md`](../../src/uix/eidos/THEMING.md) (the E1 reference) to > live as an E4 guide. The token contract holding all of this together is > [`canon/tsc.md`](../canon/tsc.md); the transversal systems every recipe > must consume are [`canon/recipe-contract.md`](../canon/recipe-contract.md). --- ## How to add a new component Assuming you already have the morfo, the soma and the eidos wrapper scaffolding (`src/uix/eidos/components/{name}/`): ### Step 1 — Decide which tokens you need Look at similar components (`button`, `toggle`, `switch`). Identify which dimensions your component exposes: - Does it have `data-color`? → palette tokens. - Does it have `data-variant`? → variant tokens. - Does it have `data-size`? → size tokens. - How many parts does it have? → per-part tokens. ### Step 2 — Add the recipe in `lib/recipes/base.ts` ```ts // Within THEME_BASE_RECIPE_TOKENS: 'my-component': { // size tokens — scope 'root' (stable) 'height-md': '36px', 'padding-inline-md': 'var(--space-3)', 'gap': 'var(--space-2)', 'radius': 'var(--radius-md)', // per-color literal definitions — scope 'root' 'primary-solid': 'var(--color-primary-solid)', 'affirm-solid': 'var(--color-affirm-solid)', 'threat-solid': 'var(--color-threat-solid)', // dynamic palette — 'host' default + per-color overrides 'palette-solid': { declarations: [ { value: 'var(--my-component-primary-solid)', scope: 'host' }, { value: 'var(--my-component-affirm-solid)', scope: 'color:affirm' }, { value: 'var(--my-component-threat-solid)', scope: 'color:threat' } ] }, // derived tokens — scope 'host' (deps inferred) 'solid-bg': { value: 'var(--my-component-palette-solid)', scope: 'host' } } ``` ### Step 3 — Regenerate ```bash npm run generate:eidos-css ``` If your config violates the TSC, the regeneration tells you: ``` Eidos recipe scope contract violations: - my-component.solid-bg (scope root): dependency 'palette-solid' is only declared at scopes [host], none of which is reachable from the consumer's scope. ``` ### Step 4 — Write the recipe CSS `src/uix/eidos/components/my-component/my-component.css`: ```css [data-my-component] { /* Private tokens — only this recipe reads them */ --_my-component-bg: var(--my-component-solid-bg); --_my-component-radius: var(--my-component-radius); display: inline-flex; align-items: center; padding-inline: var(--my-component-padding-inline-md); height: var(--my-component-height-md); border-radius: var(--_my-component-radius); background: var(--_my-component-bg); gap: var(--my-component-gap); } [data-my-component][data-disabled] { opacity: var(--opacity-disabled); pointer-events: none; } ``` ### Step 5 — Wire the CSS Code-split components import their own CSS from the wrapper `.svelte`; layout primitives and shared visuals aggregate in `index.css`: ```css @import './components/my-component/my-component.css'; ``` ### Step 6 — Verify ```bash npm run generate:eidos-css npm test -- src/uix/eidos npm run morfo:check node --import tsx/esm scripts/eidos-lint.ts my-component ``` ### Common anti-pattern: declaring composed tokens at `:root` ```ts // ❌ WRONG — the TSC will fail 'my-component': { 'palette-solid': { value: 'var(--color-neutral-solid)', scope: 'host' }, 'solid-bg': 'var(--my-component-palette-solid)' // ← implicit 'root' scope, deps live in 'host' } // ✓ CORRECT 'my-component': { 'palette-solid': { value: 'var(--color-neutral-solid)', scope: 'host' }, 'solid-bg': { value: 'var(--my-component-palette-solid)', scope: 'host' } } ``` --- ## How to define a theme Eidos supports **3 modes** of theme definition: ### Mode 1 — A patch over the base theme (recommended) Change only what you need; everything else follows the base: ```ts import { ActiveEidos } from '$uix/eidos'; ActiveEidos.create({ themeBase: { semantics: { color: { roles: { primary: 'blue', // primary uses the Radix blue scale secondary: 'plum' } } }, primitives: { typography: { families: { primary: { family: 'Inter' } } } } }, applyDom: true }); ``` ### Mode 2 — A full config For authoring from scratch: ```ts import { ActiveEidos, defineEidosConfig } from '$uix/eidos'; const config = defineEidosConfig({ primitives: { /* … */ }, semantics: { color: { /* … */ } }, themes: { /* … */ } }); ActiveEidos.create({ config, applyDom: true }); ``` ### Mode 3 — A CSS-only theme (no TypeScript) Eidos publishes the contract as empty CSS for externals to fill: ```ts const contract = activeEidos.renderContractCss({ themeSelector: "[data-theme='acme-light']" }); // Output: // [data-theme='acme-light'] { // --scale-blue-9: ; // --color-primary-solid: ; // --size-md-control-height: ; // ... // } ``` The consumer fills in the values: ```css [data-theme='acme-light'] { --scale-blue-9: #006adc; --color-primary-solid: var(--scale-blue-9); --size-md-control-height: 38px; } ``` And loads that CSS alongside Eidos's. With `themeSource: 'css'`, `ActiveEidos` generates no theme of its own. ### Versioned persistence ```ts const document = activeEidos.toDocument(); // → { kind: 'uix.eidos-config', version: 1, options: {...} } const json = activeEidos.serialize(); localStorage.setItem('user-theme', json); // Later const hydrated = createActiveEidos({ config: JSON.parse(localStorage.getItem('user-theme')!), prefs, dom }); ``` The document envelope carries a `version` for future migrations.