Three-way theme split: UI theme (context), pitch theme (store), chart preset (prop)
Theming is split into three separate systems. **UI theme** (light/dark chrome) flows through React context. **Pitch theme** (pitch colours, line style) lives in a nanostore shared across the site. **Chart preset** (editorial/analytical/minimal encoding grammar) is a component prop. Each owns one concern; they never reach into each other.
architecturethemecore-contract
Context
Three entangled problems wear “theme” as their label:
- UI chrome — page background, text colour, tooltip surface, border treatment. Changes with user light/dark preference.
- Pitch appearance — grass colour, line colour, line weight, crest styling. A cross-chart concern — every pitch in a dashboard should look the same. Changes per-site or per-demo, not per-user.
- Chart encoding grammar — which palette ramps, which default shape/size defaults, which emphasis colours. Varies per component and per editorial preset (Opta editorial vs StatsBomb analytical).
A single unified theme solves none of these cleanly. User light-dark shouldn’t repaint the pitch. Pitch-colour changes shouldn’t flip the tooltip palette. Per-chart preset changes shouldn’t cascade to chrome.
Decision
Three parallel systems, never reaching across:
- UI theme — React context (
ThemeContext). Components consume it for chrome. Default-published outputs look right in light theme; dark is opt-in. - Pitch theme — nanostore, pitch-scoped. Demos and dashboards subscribe in one place; every pitch component reads from the same store. No prop plumbing.
- Chart preset — a
presetprop on each chart component. Encoding grammar. Not stored globally because presets are editorial choices, not user preferences.
Consequences
- Swapping user light/dark never affects pitch appearance or chart encoding.
- The pitch store is a site concern, not a library concern — other apps using Campos can wire their own store or pass colours as props; the library does not publish a pitch-theme singleton.
- Data-encoding palettes live in compute, not in any theme system (see
style-injection-callback-first). - New concerns that sound like “theme” must answer “which of the three?” before they get added; the split is the design constraint that prevents re-entanglement.