Files
2026-01-19 19:02:32 +01:00

7.1 KiB
Raw Permalink Blame History

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

  1. Single source of truth for theme tokens (colors, typography, radii, shadows).
  2. Semantic tokens (intent-based), not implementation tokens (utility-class-based).
  3. Runtime theme selection via CSS variables (SSR-correct by default).
  4. UI components remain pure and follow docs/architecture/website/COMPONENT_ARCHITECTURE.md.
  5. 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):

  • Theme
    • color
      • bg.base
      • bg.surface
      • bg.surfaceMuted
      • border.default
      • text.high
      • text.med
      • text.low
      • intent.primary
      • intent.warning
      • intent.success
      • intent.critical
    • radius
      • sm md lg xl
    • shadow
      • sm md lg focus
    • font
      • body mono heading

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 variables are the runtime truth.
  • TypeScript theme contract is the compile-time truth.

The theme is a public contract in the UI layer:

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=default on the <html> element in RootLayout.

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.


Tailwinds 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/ui never 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:

  1. Use CSS Modules for component styling.
  2. Use semantic variants and intent props.
  3. Use CSS variables for actual values.

Example conceptually:

  • Button
    • variant: primary | secondary | ghost
    • intent: default | danger
    • CSS module uses variables like background: var(--ui-color-intent-primary).

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:

  1. Prefix with --ui- to avoid collisions.
  2. Use semantic names (surface, text, intent), not “brand” names.
  3. Avoid numeric-only palettes in component code (no gray-600 in UI internals).

Adding themes (extensibility model)

To add a new theme:

  1. Keep the token contract stable (same variables).
  2. Provide a new theme override block in apps/website/ui/theme/theme.css, e.g.:
    • :root[data-theme=default] { ... }
    • :root[data-theme=brand-x] { ... }
  3. Optionally provide a matching TypeScript definition in apps/website/ui/theme/themes for 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)

  1. Token changes are localized to the theme definition (CSS vars) rather than spread across UI components.
  2. UI components become portable: they do not depend on any particular styling framework.
  3. It becomes possible to add enforcement (lint rules, conventions) around “no Tailwind tokens inside UI”.
  4. Visual identity evolves without turning into a codebase-wide search-and-replace.