7.8 KiB
Website UI Theming Concept
This document defines the conceptual theming architecture for the Website UI layer. It is written to make the UI maintainable, themeable, and decoupled from Tailwind (Tailwind may exist as an implementation detail, never as the source of truth).
For the existing visual direction and palette intent, see docs/THEME.md.
Problem
The current UI style surface mixes:
- semantic intent (primary, surface, outline)
- concrete implementation (Tailwind class strings)
- and multiple naming systems (Tailwind tokens, ad-hoc names, CSS variables)
This makes it hard to:
- change colors consistently,
- introduce additional themes,
- reason about component contracts,
- and enforce architectural boundaries in
apps/website/ui.
Goals
- Single source of truth for theme tokens (colors, typography, radii, shadows).
- Semantic tokens (intent-based), not implementation tokens (utility-class-based).
- Runtime theme selection via CSS variables (SSR-correct by default).
- UI components remain pure and follow
docs/architecture/website/COMPONENT_ARCHITECTURE.md. - Tailwind is optional and must remain a consumer/adapter, not the authoring surface.
Non-goal for this iteration: shipping a user-facing theme switcher. We implement a single default theme but design the architecture to add more later.
Core idea: Theme contract + CSS variables boundary
1) Theme contract (TypeScript)
We define a typed theme contract, e.g. Theme, that expresses the semantic palette and style tokens.
Principle:
- UI components accept semantic props (variant, intent, emphasis).
- UI components do not accept Tailwind token strings like
bg-panel-gray.
Example shape (conceptual):
Themecolorbg.basebg.surfacebg.surfaceMutedborder.defaulttext.hightext.medtext.lowintent.primaryintent.warningintent.successintent.critical
radiussmmdlgxl
shadowsmmdlgfocus
fontbodymonoheading
The exact token taxonomy can evolve, but the rule is stable: tokens represent meaning, not implementation.
2) CSS variables as the runtime boundary
The theme is applied by setting CSS custom properties. UI components reference variables, not Tailwind classes.
The canonical variables are defined in a dedicated stylesheet, e.g. docs/architecture/website/UI_THEMING.md describes this as: --ui-color-bg-base, --ui-color-text-high, etc.
Implementation typically lives in apps/website/ui/theme/theme.css and is imported by apps/website/app/globals.css.
The existing variables in apps/website/app/globals.css are already a strong start. The key change is to treat them as the UI contract and stop treating Tailwind names as the contract.
Where theme lives (layering)
This respects the UI purity rules in docs/architecture/website/COMPONENT_ARCHITECTURE.md.
Authoritative token source
CSS variablesare the runtime truth.TypeScripttheme contract is the compile-time truth.
The theme is a public contract in the UI layer:
apps/website/ui/theme/Theme.tsapps/website/ui/theme/themes/default.tsapps/website/ui/theme/theme.css
Applying the theme (SSR correctness)
SSR correctness means: the initial HTML response already knows which theme is active.
For a single default theme, the simplest SSR-correct approach is:
- set
data-theme=defaulton the<html>element inRootLayout.
Later, if theme becomes user-selectable, the server can choose the theme per request using cookies/headers in RootLayout, still using the same CSS-variable mechanism.
Tailwind’s role (strictly non-authoritative)
Tailwind may remain in the project for page/layout scaffolding or legacy areas, but it must be treated as:
- a consumer of theme variables (e.g. Tailwind config maps colors to
var(--ui-...)), or - an adapter for non-UI-layer code.
In other words:
- UI components inside
apps/website/uinever rely on Tailwind class name semantics. - If Tailwind is used, it only references CSS variables (so switching the theme changes the look without changing class names).
This prevents “utility token drift” where a Tailwind rename becomes a breaking theme change.
Component authoring rules
Components are theme-driven, not class-driven
Inside apps/website/ui:
- Use CSS Modules for component styling.
- Use semantic variants and intent props.
- Use CSS variables for actual values.
Example conceptually:
Buttonvariant: primary | secondary | ghostintent: default | danger- CSS module uses variables like
background: var(--ui-color-intent-primary).
Primitives
The primitives in apps/website/ui/primitives exist to build consistent semantics. They should not encode a “theme” directly; they expose minimal layout/styling mechanics.
Examples in code today:
Conceptually, primitives should either:
- map typed props to standard CSS properties (style object), or
- map typed props to CSS variables that are already in the theme contract.
What primitives should not do: generate Tailwind class strings as their main behavior.
Token naming: semantic + stable
Recommended CSS variable naming:
--ui-color-bg-base--ui-color-bg-surface--ui-color-border-default--ui-color-text-high--ui-color-intent-primary--ui-radius-md--ui-shadow-md--ui-font-body
Rules:
- Prefix with
--ui-to avoid collisions. - Use semantic names (surface, text, intent), not “brand” names.
- Avoid numeric-only palettes in component code (no
gray-600in UI internals).
Adding themes (extensibility model)
To add a new theme:
- Keep the token contract stable (same variables).
- Provide a new theme override block in
apps/website/ui/theme/theme.css, e.g.::root[data-theme=default] { ... }:root[data-theme=brand-x] { ... }
- Optionally provide a matching TypeScript definition in
apps/website/ui/theme/themesfor tooling and validation.
The important property: components do not change when a new theme is added.
Architecture view
flowchart TD
A[Semantic theme contract] --> B[CSS variables]
B --> C[UI components]
B --> D[Tailwind adapter]
C --> E[App components]
Interpretation:
- Theme contract defines the tokens we promise.
- CSS variables implement those tokens at runtime.
- UI components consume tokens directly.
- Tailwind, if present, only consumes variables.
Consequences (why this improves maintainability)
- Token changes are localized to the theme definition (CSS vars) rather than spread across UI components.
- UI components become portable: they do not depend on any particular styling framework.
- It becomes possible to add enforcement (lint rules, conventions) around “no Tailwind tokens inside UI”.
- Visual identity evolves without turning into a codebase-wide search-and-replace.