Docs
Theming, Config-First

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
AxisValuesWhat it controls
brandyour own keys (default, acme…)which seed colors derive the palette
modelight / darkthe canonical seed swap (baseLight↔baseDark, deepLight↔deepDark) + the surface ladder — see Dark Mode Engine
surfacepositive / negativecontainer-level inversion — see Surface Inversion
dimensioncompact / normal / spaciousdensity 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.

One config, twelve variants

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 onLight ink whose contrast against its light fill 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.designMd emits dist/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