--- title: Eidos Theming — Comparison + decisions type: notes audience: human + agent status: current source: migrated from src/uix/eidos/THEMING_NOTES.md (2026-07-02, docs-book F7.3; originally extracted from THEMING §15 + §17) --- # Eidos Theming — Comparison + decisions > The theming's E3 material: how eidos compares with the reference libraries, > and the FAQ of the controversial decisions (the *why* behind the choices). > Extracted from [`THEMING.md`](./reference.md) (the E1 > reference). --- ## Comparison with reference libraries ### Bundle size (a typical 10-component app) | System | Raw | Gzip | Strategy | |---|---|---|---| | Tailwind v4 | ~30 KB | ~10 KB | JIT atomic classes | | **Eidos + purge** | **116 KB** | **13.6 KB** | **JIT custom-property purge** | | Chakra Panda v3 | ~30 KB | ~12 KB | Build-time JIT | | Mantine | ~60 KB | ~18 KB | No purge | | Radix Themes | ~80 KB | ~22 KB | No purge | | shadcn/ui | varies | varies | Copy-paste, not centralized | | **Eidos without purge** | **218 KB** | **25 KB** | Single CSS | ### Theming features | Feature | Eidos | Radix Themes | Chakra Panda | Mantine | shadcn | Tailwind v4 | |---|---|---|---|---|---|---| | **Token scope as data** | ✅ TSC | ❌ implicit | ⚠️ build-time | ❌ runtime | ❌ N/A | ❌ N/A | | **Auto-inferred deps** | ✅ `var()` parse | ❌ | ✅ types | ❌ | N/A | N/A | | **Cross-axis collision** | ✅ explicit | ❌ | ⚠️ partial | ❌ | N/A | N/A | | **Composite scopes** | ✅ `[axis:v, …]` | ❌ | ✅ conditional pairs | ❌ | N/A | N/A | | **9 canonical color roles** | ✅ the book | ⚠️ 6 accents | ❌ open | ❌ open | ⚠️ 4 roles | ❌ open | | **Coordinated size canon** | ✅ canon + guard + consumed¹ | ⚠️ 1-3 | ⚠️ 5 | ⚠️ 5 | ❌ | N/A | | **Runtime density** | ✅ 3 levels | ❌ | ❌ | ⚠️ partial | ❌ | ❌ | | **Contract introspection** | ✅ typed | ⚠️ docs | ✅ Panda | ⚠️ docs | ❌ | ❌ | | **Runtime override** | ✅ contract-aware | ⚠️ via CSS vars | ❌ | ✅ CSSVarsProvider | ⚠️ via CSS | ❌ | | **Versioned persistence** | ✅ envelope | ❌ | ❌ | ❌ | ❌ | N/A | | **Perceptual layer (sema)** | ✅ unique | ❌ | ❌ | ❌ | ❌ | ❌ | ¹ Full coordination: the `--size-{k}-*` bundle is emitted, guarded (no px/rem literals in recipe `font-size-*`/`icon-size-*`), **and consumed** — the C7 sweep (2026-07-03) pointed all 34 size-bearing recipes at the bundle coordinate (`--size-{k}-control-height` / `-font-size` / `-icon-size`) instead of the raw primitive of the same coordinate, and `recipe-css-contract` forbids the raw primitive so the drift cannot return. The size→font mapping is the universal **1:1** of the size canon (§5; the 2026-06-15 audit's `control · compact · dense` archetypes were superseded by the 2026-06-17 override — control text follows the typographic scale 1:1). Hence ✅ (canon + enforcement + consumption). ### Mental model | System | Token philosophy | |---|---| | Eidos | 7 layers of indirection, scope-as-contract, perceptual integration | | Radix Themes | 3 layers, runtime accent swap, no scope contract | | Chakra Panda | Build-time conditional values, a recipe system | | Mantine | Runtime theme provider, string interpolation | | shadcn | Flat `--primary` + `.dark`, copy-paste components | | Tailwind v4 | The `@theme` directive, atomic utilities, no composed tokens | **Critical read**: Eidos is NOT simpler than Tailwind nor more ergonomic than shadcn. It is **more expressive** on the dimension "what a component can communicate". If your app only needs a primary color and a dark mode, shadcn is the answer. If your app needs to perceptually differentiate "save draft" (affirm) from "delete permanently" (loss) with distinct tokens and animations, Eidos is the system. --- ## FAQ — controversial decisions ### Why doesn't theming live in Morfo? Shouldn't Morfo be the source of truth for everything? A CRITICAL question — the detailed answer is [`THEMING.md`](./reference.md) §1.bis. Summary: morfo is the source of truth of the **cross-layer contract** (parts, events, attrs, archetypes, attr values). Tokens/themes/recipes belong to Eidos by explicit architectural design ([`architecture/active-architecture.md`](../architecture/active-architecture.md) §9). The 2-of-3 rule derives it: visual tokens are consumed by eidos alone → 1-of-3 → they don't enter morfo. The eidos↔morfo integration happens **through the DOM** (recipes target the attrs morfo declares), NOT by importing morfo objects in TS (rule #6 forbids it explicitly). ### Why 7 layers of indirection? It looks excessive Each layer serves a real override point: - Without layer 1, you can't bring a custom Radix palette. - Without layer 2, you can't remap roles. - Without layer 3, you can't tune slots per role. - Without layer 4, you can't override a color for ONE component only. - Without layer 5, you can't have a per-instance dynamic palette. - Without layer 6, you can't combine variant × palette. - Without layer 7, recipes would mix external and internal tokens. Layer 4 is the most redundancy-suspect. It is a deprecation candidate if after 6 months no consumer uses it. ### Why the TSC and not just convention? **Convention fails silently.** The pre-TSC Toggle bug would have stayed hidden for years. With the TSC, the build fails. It is the difference between "you should do it right" and "you cannot do it wrong". ### Why not use Tailwind since it's smaller? Tailwind: - Has no canonical semantic roles (success/danger/warning exist but they are Bootstrap-world). - Has no perceptual layer (sema). - Has no runtime density. - Has no programmatic contract introspection. But if your app is simple, **use it**. Eidos justifies its complexity only when the app needs the dimensions Eidos covers. ### Why not atomic classes like Tailwind? Custom properties enable: - **Dynamic cascade** (runtime palette overrides). - **Theme switching** without a recompile. - **Persistence** of the user's theme. - **Composability** with sema (the `event:*` scope). Atomic classes compress better but are static. It is impossible to make `--palette-solid` change with `data-color='affirm'` from atomic classes without generating ×N variants at build time. ### Why invent the "TSC" instead of using CSS's native @scope? `@scope` (CSS Cascading Modules L6) is bleeding-edge: Chrome 118+, Firefox 128+, Safari not yet. Not production-ready in 2026. When @scope is universal, the TSC could be re-implemented on top of it. The config surface (declarations[] + scope) would stay the same; only the emitted CSS would change. ### Why `event:` in the TSC instead of the motion doc's `data-motion-ref`? > **SUPERSEDED (2026-07-11, DOC-4).** The motion REDESIGN resolved this > question differently: event-driven visuals are **motion SIGNATURES** > (`EidosConfig.motion.signatures` — generic per family/intent/event, > generated CSS; see `theming/motion.md` and reference §13/§14). The > `scope: 'event:*'` TSC route described below was never exercised and is no > longer the plan. Kept as the rationale record of the moment. Three reasons: 1. **The TSC already exists and works.** `data-motion-ref` would require a new DOM attribute, a runtime to inject it, a separate registry. 2. **Sema already emits `data-event`.** Reusing it costs zero architecture. 3. **Composable**: `scope: ['event:announce', 'color:affirm']` lets the animation differ by valence. `data-motion-ref` would lose that or need more complex keys. ### What's next? Deferred pendings: - Migrate the 5 palette consumers (button, checkbox, switch, radio-group, toggle-group) to `declarations[]` for uniformity. - ~~Implement the first real `scope: 'event:*'` case~~ — superseded by motion signatures (see the note above). - Decide whether to collapse layer 4 (component-color) — deferred until a consumer asks for that extension point (on the F4-D decision menu of the clean-room plan, 2026-07-11).