Theming, Config-First
Apollion themes are derived, not hand-painted. You supply a handful of brand seed colors; the engine derives everything else — full palettes, light/dark, both surfaces, three densities — deterministically, the same way at runtime and at build time.
The four axes
Every resolved theme is one cell of a cartesian product:
brand × mode × surface × dimension| Axis | Values | What it controls |
|---|---|---|
| brand | your own keys (default, acme…) | which seed colors derive the palette |
| mode | light / dark | the canonical seed swap (baseLight↔baseDark, deepLight↔deepDark) + the surface ladder — see Dark Mode Engine |
| surface | positive / negative | container-level inversion — see Surface Inversion |
| dimension | compact / normal / spacious | density multiplier on spacing/typography — see Dimensions |
One brand × the default axes = 12 derived variants, all from the same ~14 seed colors.
Runtime: createTheme
For apps that render the theme in React — the common case:
import { ApollionProvider, createTheme } from '@apollion-dsi/core/themes';
const theme = createTheme({
colors: {
main: '#003750',
complementary: '#F6BA20',
primary: '#32AFDC',
secondary: '#2D81AA',
tertiary: '#2CE571',
success: '#2CB567',
warning: '#F6BA20',
danger: '#E12712',
information: '#3399FF',
baseLight: '#FCFCFC',
baseDark: '#26292E',
deepLight: '#FFF',
deepDark: '#000',
},
dimension: 'normal',
});
function App({ children }) {
return <ApollionProvider theme={theme}>{children}</ApollionProvider>;
}ApollionProvider also accepts lightTheme/darkTheme pairs and
useSystemPreference to follow the OS's prefers-color-scheme — see
ApollionProvider.
Type weight origin
typeWeight ('light' | 'regular' | 'stronger') is the brand-level origin of
the weight ladder, the way dimension is for size. The four semantic weights
resolve from a table — regular is the stock ladder (300 / 400 / 600 / 900),
light starts one step lighter (300 / 300 / 400 / 600), stronger one step
heavier (400 / 600 / 700 / 900). Brands stop remapping font.fontWeight by
hand; call-site fontWeight still overrides.
createTheme({ typeWeight: 'light' });
<ApollionProvider typeWeight="light">…</ApollionProvider>;For a static CSS/JSON/TS surface — CI artifacts, SSG output, non-React
consumers, or design tooling — apollion.config.mjs expresses the same
seeds as data:
// apollion.config.mjs
import { defineConfig } from '@apollion-dsi/tokens/config-loader';
export default defineConfig({
brands: {
default: {
baseDark: '#26292E',
baseLight: '#FCFCFC',
deepDark: '#000',
deepLight: '#FFF',
main: '#003750',
complementary: '#F6BA20',
primary: '#32AFDC',
secondary: '#2D81AA',
tertiary: '#2CE571',
success: '#2CB567',
warning: '#F6BA20',
danger: '#E12712',
information: '#3399FF',
},
},
modes: ['light', 'dark'],
surfaces: ['positive', 'negative'],
dimensions: ['compact', 'normal', 'spacious'],
output: { css: true, json: true, ts: true, designMd: true },
});apollion-tokens build --config apollion.config.mjs --out dist/Runtime and build-time share the exact same derivation code (a parity test
suite pins it), so a color picked in createTheme and the same seed built
through apollion-tokens produce byte-identical output.
brand=default × mode={light,dark} × surface={positive,negative} × dimension={compact,normal,spacious}
What the engine guarantees for free
- AA by construction — every named palette emits an
onLightink whose contrast against itslightfill is ≥ 4.5:1, derived, not hand-picked. - Real dark, not build-time = light. Dark mode is the canonical seed swap through the same engine — mode-aware state floors, tonal pairs and neutral ramps all resolve correctly on dark by construction.
- Deterministic builds. Same config + same environment → byte-identical
dist/, sha256-fingerprinted in a manifest — safe to cache on. - An AI-context artifact for free.
output.designMdemitsdist/design/<brand>.design.md— resolved values per variant, generated from your seeds, so it never drifts from what you shipped. See Output formats.
Breakpoints travel with the tokens
The viewport breakpoint scale (xs 0 · sm 575 · md 767 · lg 990 · xl 1200,
px) is part of the token contract, not a React-only detail: the tokens build
emits it as breakpoint.* in JSON/DTCG/TS, as informational
--apollion-breakpoint-* CSS vars, and as a literal-px Tailwind screens
scale. Bands are half-open [value, next) over ascending min-width sets —
mobile is the unprefixed base, wider contexts opt in. The React factory reads
the same values (theme.breakpoints), so a non-React consumer and a sm_*
prop always agree on where a band starts.
Overrides are an escape hatch, not the default
Every derived value can be overridden per slot when you have a real reason — the engine respects it verbatim, including skipping the guarantees above for that slot. Prefer a new Foundation alias over a literal override; see Semantic & Foundation Tokens.
See also
- Create Theme — the full
createThemeAPI. - Build CLI —
apollion-tokensflags and the variant matrix. - Output formats — CSS / JSON / TS / Design.md.
- Brand examples — two full reference configs.