Themes ​

A theme is a named ThemeDefinition: an id, a token map, and an optional colorScheme ("light" or "dark"). The controller registers one or more themes and resolves which one is active from mode, system preference, and the current theme id.

Overview ​

Themes are registration units. Mode (light / dark / system) decides which scheme and which of lightThemeId / darkThemeId to prefer. setTheme selects a concrete id and may exit system mode when you pin a theme while mode was system.

When to use ​

  • Shipping light and dark (or multi-brand) token sets
  • Mapping OS preference to specific theme ids
  • Registering themes at runtime (feature flags, white-label)

When not to use ​

Installation ​

See Installation. Presets:

ts
import { lightTheme, darkTheme, lightTokens, darkTokens } from "@sometic/theme/presets";

Usage ​

Register themes with the controller ​

ts
import { createThemeController } from "@sometic/theme";
import { lightTheme, darkTheme } from "@sometic/theme/presets";

const theme = createThemeController({
    themes: [lightTheme, darkTheme],
    defaultThemeId: lightTheme.id,
    lightThemeId: lightTheme.id,
    darkThemeId: darkTheme.id,
    mode: "system",
    density: "comfortable",
    direction: "ltr",
});

Custom theme definition ​

ts
import { defineTokens } from "@sometic/theme/tokens";
import type { ThemeDefinition } from "@sometic/theme";

const oceanTokens = defineTokens({
    color: {
        bg: "#0b1f2a",
        fg: "#e8f4f8",
        primary: "#38bdf8",
        danger: "#f87171",
    },
    space: { 1: "0.25rem", 2: "0.5rem", 3: "0.75rem", 4: "1rem" },
    radius: { sm: "0.25rem", md: "0.5rem", lg: "0.75rem" },
});

const oceanDark: ThemeDefinition = {
    id: "ocean-dark",
    colorScheme: "dark",
    tokens: oceanTokens,
};

Switch mode and theme id ​

ts
theme.setMode("dark"); // mode dark + themeId → darkThemeId
theme.setMode("light"); // mode light + themeId → lightThemeId
theme.setMode("system"); // follow prefers-color-scheme

theme.setTheme("ocean-dark"); // pins that id; if mode was system, mode becomes colorScheme of that theme (or "light")

Register / unregister at runtime ​

ts
theme.registerTheme(oceanDark);

theme.unregisterTheme("ocean-dark");

You cannot unregister the last remaining theme. Unregistering the active themeId falls back to another registered id.

How it works ​

Resolution order (simplified) ​

  1. Read preferences (mode, themeId, density, direction, flags).
  2. Resolve resolvedColorScheme:
    • mode === "system" → OS dark ? "dark" : "light" (no-preference treated as light for scheme)
    • else → "dark" or "light" from mode
  3. Pick preferred theme id:
    • system → darkThemeId or lightThemeId
    • else → preferences.themeId
  4. Fallbacks if missing: themeId → lightThemeId → first registered theme.
  5. Build CSS variables from that theme’s tokens; build attributes.

setMode("dark") and themeId ​

Setting mode to "dark" or "light" also moves themeId to darkThemeId / lightThemeId so resolved tokens match the scheme. Setting "system" updates mode only and lets light/dark ids drive resolution from the OS.

Snapshot attributes ​

AttributeSource
data-themeresolvedThemeId
data-color-schemeresolvedColorScheme
data-densitypreferences.density
dirpreferences.direction
data-high-contrastpresent when resolved high contrast is true
data-reduced-motionpresent when resolved reduced motion is true

API ​

ThemeDefinition ​

FieldTypeRequiredDescription
idstringyesStable theme id
tokensThemeTokensyesToken map
colorScheme"light" | "dark"noUsed when setTheme exits system mode
OptionTypeDefaultDescription
themesreadonly ThemeDefinition[](required)Initial registry; must be non-empty
defaultThemeIdstring(required)Must exist in themes
lightThemeIdstringdefaultThemeIdPreferred id for light / system-light
darkThemeIdstringdefaultThemeIdPreferred id for dark / system-dark
modeThemeMode"system"Initial mode
densityThemeDensity"comfortable"Comfortable / compact / spacious / custom string
directionThemeDirection"ltr"ltr or rtl
highContrastSystemAwareFlagfalsetrue | false | "system"
reducedMotionSystemAwareFlag"system"true | false | "system"
prefixstring"sometic"CSS variable prefix
persistbooleanfalsePrefer Theme store
storage / storageKeyadapter / stringsee store docsPersistence

Controller theme methods ​

MethodDescription
registerTheme(theme)Add or replace by id; rebuilds snapshot
unregisterTheme(id)Remove; throws if last theme; no-op if unknown
setMode(mode)Update mode (and themeId for light/dark)
setTheme(themeId)Select id; throws if unknown
setDensity / setDirection / setHighContrast / setReducedMotionPreference setters

Presets (@sometic/theme/presets) ​

ExportDescription
lightTokens / darkTokensMinimal token maps
lightTheme / darkTheme{ id, colorScheme, tokens }

Edge cases ​

CaseBehavior
Empty themes arrayThrows at create
Unknown defaultThemeIdThrows at create
Unknown setTheme(id)Throws
Unregister last themeThrows
Unregister active themeSwitches themeId to another registered id
mode: "system" + OS changeSnapshot rebuilds when scheme subscription fires
Missing preferred idFalls back through themeId → lightThemeId → first theme

FAQ ​

What does setMode("dark") do to themeId? ​

It sets mode to dark and moves themeId to darkThemeId so tokens match.

Why aren’t presets on the root entry? ​

Size budget: keep @sometic/theme lean; import presets when needed.

Can I have more than two themes? ​

Yes. Register as many as you need. lightThemeId / darkThemeId only matter for light/dark/system resolution shortcuts.

Do themes persist automatically? ​

Only preference fields persist when persist: true. Token maps live in memory (or your module graph). See Theme store and Store.

Is colorScheme on the definition required? ​

No. It matters when setTheme is called while mode is system: mode becomes theme.colorScheme ?? "light".