name: theme-system-design
# description: routing-facing summary of when this skill activates and what it covers.
description: "Use when designing a theme system — design tokens, semantic token layering, CSS custom property strategy, runtime theme switching, and theme contract guarantees. Do NOT use for one-off color choices, brand-only palette work, or framework-specific styling-library configuration."
# license: SPDX-compatible license identifier for the skill content.
license: CC-BY-4.0
# metadata: Skill Metadata Protocol fields encoded under Agent Skills-compatible frontmatter.
metadata:
# === v8 Classification (subject + public; polyhierarchy via subjects[]) — see ADR-0017 ===
# subject: primary browse shelf — what the skill teaches. One of twelve closed values:
# backend-engineering / frontend-engineering / software-architecture / data-engineering / agent-ops / ai-engineering /
# quality-assurance / design / reasoning-strategy / software-engineering-method / knowledge-organization / product-domain.
subject: frontend-engineering
# public: publishability/private-data gate. Boolean.
# true = publishable/shareable; false = private and excluded from public export.
# Project anchoring is carried separately by non-empty `project[]` plus `grounding`.
public: true
# scope: free-text PRD-style statement of what the skill teaches and what it excludes.
# (v8 required; not an enum). Mirrors Coverage + Do NOT Use When at frontmatter level.
scope: "Designing a theme system — a tiered token contract (reference → system/semantic → component), CSS custom property delivery, runtime theme switching (persistence, pre-paint application, propagation), and additive-vs-breaking theme contract guarantees. Portable across web design systems; principle-grounded, not repo-bound. Excludes one-off color choices, palette/brand-value design, dark-mode platform detection, and styling-library runtime configuration."
# taxonomy_domain: optional hierarchical sub-path within `subject`. Slash-delimited
# lowercase kebab-case segments. rename of the original v8 `domain`. Remove when the flat
# `subject` is sufficient.
taxonomy_domain: engineering/frontend
# stability: lifecycle marker. One of:
# experimental (active development) / stable (production-ready) /
# frozen (no further changes expected) / deprecated.
# When `deprecated`, schema's allOf REQUIRES `superseded_by: <real-skill-name>`.
stability: experimental
# keywords: semantic phrases for fuzzy router activation. v8 cap: max 10.
# Keep terms a user would actually type when starting a task in this skill's domain.
keywords: ["theme token contract","theme semantic layer","theme variables","css custom properties","runtime theme switching","token tiers","theme contract","design tokens community group","theme parity","token naming"]
# triggers: explicit-match activation phrases the router fires on literally.
# Use when label-based routing is intended; usually keywords + examples are enough.
triggers: ["theme system","design tokens","theme switching","css variables for theming","token architecture"]
# examples: 2-5 realistic user prompts the skill SHOULD activate for.
# Written in the user's voice. Improves retrieval recall beyond keywords alone.
examples: ["Design a three-tier token system (reference → system → component) for a multi-brand product","Add a third theme to an existing two-theme system without breaking the component contracts","Move from hard-coded colors to CSS custom properties with runtime switching"]
# anti_examples: near-miss prompts that should route ELSEWHERE.
# Pair with relations.suppresses (or legacy boundary alias) to name the confusable territory's owner.
anti_examples: ["Choose the exact hex value for the brand's primary blue","Configure Tailwind's content array and purge settings","Implement the dark mode toggle interaction"]
# relations: typed graph edges to sibling skills. Current fields:
# related (adjacency for browse / co-routing expansion) /
# suppresses (exclude listed skills from co-routing when THIS skill wins; write reason
# as "I own this exclusively over X", not "use X instead") /
# boundary (DEPRECATED alias of suppresses, retained for unmigrated skills) /
# verify_with (cross-check; co-loaded as one-hop expansion) /
# depends_on (composition; transitive — A→B→C loads all three) /
# broader / narrower (SKOS-style generalization) /
# disjoint_with (mutual exclusion for incompatible ownership).
relations: "{\"related\":[\"color-system-design\",\"design-system-architecture\",\"visual-design-foundations\",\"dark-mode-implementation\",\"typography-system\"]}"
# === Understanding fields (when comprehension_state: present) ===
# mental_model: the primitives of the concept and how they relate. One paragraph.
mental_model: |
A theme system is an indirection layer between design decisions and component code, expressed as named tokens in tiers. Reference tokens are raw values (blue.500 = #1E66F5); system/semantic tokens name intent (color.background.surface, color.text.primary); component tokens are component-specific overrides (button.primary.background). Components consume only system and component tokens — never raw values or reference tokens — and a theme is a mapping from reference tokens into the system layer. Delivered at runtime via CSS custom properties scoped to a selector (:root for default, [data-theme="dark"] for alternates), which cascade and inherit so a subtree override is a single attribute change. Runtime switching has three pieces: persistence (localStorage/cookie), pre-paint application (a class or data attribute set before first paint to avoid flash), and propagation (notifying components that read theme imperatively, like chart libraries or canvases).
# purpose: the problem this concept solves and why the field exists. One paragraph.
purpose: |
Without the indirection, every component hard-codes appearance: a color value or a name like gray.100 that happens to be the surface color today. That collapses three different rates of change into one — brand values change rarely, theme assignments (which brand color means "danger") change occasionally, and components change continuously — so every theme or brand change forces edits across every component. A theme system exists to separate those rates: components depend on stable intent-named tokens; themes swap the underlying values; brand decisions live in the reference layer. The result is that a single theme swap repaints the whole product with no component edits, a new theme is added by providing only a new set of system-token values, and a renamed or removed token surfaces as a build/lint error rather than a silent runtime fallback.
# concept_boundary: what this concept is NOT. Distinguishes from adjacent skills by naming
# the MECHANISM that differs, not just the label. Canonical replacement for top-level `boundary`.
concept_boundary: |
This is NOT picking specific color values or designing a palette (color-system-design owns what blue.500 is and how the palette is structured) — theme-system-design owns the indirection contract and switching mechanism that consume those values. It is NOT dark-mode platform integration (dark-mode-implementation owns prefers-color-scheme detection, OS-preference handling, and asset variants) — theme-system-design owns the general N-theme token contract that dark mode is one application of. It is NOT one-off styling of a single component with no system-wide implications (design-module-composition), NOT typography or spacing scale design (typography-system / visual-design-foundations), and NOT configuring a styling library's runtime (Tailwind config, Emotion ThemeProvider setup) when the token contract is unchanged — that is build configuration, not theme architecture.
# analogy: one-sentence metaphor preserving the core mechanism.
analogy: "A theme system is to a product's appearance what a translation layer is to a multilingual app — components speak in stable intent words (\"surface\", \"danger\"), and the active theme is the dictionary that resolves each word to a concrete value, so swapping the dictionary repaints everything without rewriting a single component's vocabulary."
# misconception: the wrong mental model people bring; corrected explicitly.
misconception: |
The wrong mental model is "a theme system is a set of color variables you toggle." That treats tokens as a flat list and themes as a light/dark switch over raw values. The corrected model: the value of the system is the TIERED INDIRECTION and the SEMANTIC NAMING, not the variables themselves. Semantic names (color.background.surface) carry intent so a component author knows what to use without knowing today's value; descriptive names (color.gray.100) leak appearance into the contract and break meaning when the theme flips. The discipline is to resist convenience names, keep components off the reference layer entirely, and treat token rename/removal as a breaking change with a deprecation cycle — a flat variable list with no tiers provides none of that.
A theme system is a contract between design decisions and component code, structured as named tokens arranged in tiers so that appearance can change without touching the components that render it. Its core mechanism is indirection: components consume intent-named tokens (a "surface" color, a "primary text" color), and a theme supplies the mapping from those intent names to concrete values. The mainstream model uses three tiers — reference tokens (raw values like blue.500 = #1E66F5), system or semantic tokens (intent-named like color.background.surface), and component tokens (component-specific overrides like button.primary.background) — with components allowed to read only the system and component tiers. On the web the runtime delivery vehicle is CSS custom properties scoped to a selector (:root for the default, [data-theme="dark"] for alternates), which cascade and inherit so that overriding a subtree's theme is a single attribute change. Runtime switching adds three operational concerns — persisting the choice, applying it before first paint to avoid a flash, and propagating it to components that read theme imperatively. What the system buys is that three different rates of change (rarely-changing brand values, occasionally-changing theme assignments, continuously-changing components) each get their own surface to evolve on, so a theme swap repaints the whole product, a new theme is just a new set of system-token values, and a removed token fails loudly at build time instead of silently at runtime.
A theme system is a contract between design decisions and component code, expressed as named tokens that components consume and themes resolve. The mainstream model uses three tiers — reference tokens (raw values: blue.500 = #1E66F5), system or semantic tokens (intent-named: color.background.surface, color.text.primary), and component tokens (component-specific overrides: button.primary.background). Components consume system and component tokens only; themes provide reference-token-to-system-token mappings. This indirection is what allows a single theme swap to repaint the whole product without component edits.
CSS custom properties are the dominant runtime delivery mechanism for web themes. A theme is a set of custom-property assignments scoped to a selector — typically :root for the default, [data-theme="dark"] for alternates. Custom properties cascade and inherit, so a theme override on a subtree (a dark-on-light landing-page hero inside a light app) is a single attribute change. The W3C Design Tokens Community Group format (design-tokens.org) is the emerging interchange standard, expressed as JSON with $value, $type, and $description; tooling (Style Dictionary, Tokens Studio, Terrazzo) transforms it into platform outputs (CSS variables, iOS catalogs, Android resources).
Runtime switching has three operational pieces: persistence (localStorage or a cookie if SSR-sensitive), application (a class or data attribute on the document root before first paint to avoid flash), and propagation (notify components that read theme imperatively — chart libraries, canvases). The pre-paint application typically requires a small inline script in the document head that runs before the stylesheet loads.
Theme contracts make additive changes safe and breaking changes visible. Adding a new system token is safe; renaming or removing one is a breaking change requiring a deprecation cycle. Components that read tokens by exact name should not be expected to handle missing tokens gracefully — a missing token resolves to the CSS variable's fallback value (or invalid, depending on the property), which is rarely what's wanted in production.
The indirection earns its complexity by separating two rates of change: brand decisions change rarely, theme assignments (which brand color means "danger") change occasionally, and components change continuously. A flat token system collapses these into one rate and forces every component to know about every brand value. The three-tier model gives each rate of change its own surface to evolve on.
Semantic names beat descriptive names. color.background.surface tells a component author what to use; color.gray.100 forces them to know that gray.100 happens to be the surface color today and changes meaning when the theme flips. The discipline is to resist convenience names that leak appearance into the contract.