9.3 KiB
| title | type | audience | status | source |
|---|---|---|---|---|
| Eidos Theming — Comparison + decisions | notes | human + agent | current | 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(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 §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
§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; seetheming/motion.mdand reference §13/§14). Thescope: 'event:*'TSC route described below was never exercised and is no longer the plan. Kept as the rationale record of the moment.
Three reasons:
- The TSC already exists and works.
data-motion-refwould require a new DOM attribute, a runtime to inject it, a separate registry. - Sema already emits
data-event. Reusing it costs zero architecture. - Composable:
scope: ['event:announce', 'color:affirm']lets the animation differ by valence.data-motion-refwould 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— superseded by motion signatures (see the note above).scope: 'event:*'case- 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).