Theming

@sometic/theme is the runtime design-token and theme engine for Sometic. It owns token maps, CSS variable generation, system preference detection, WCAG contrast helpers (hex today), light/dark presets, and a framework-neutral theme controller that produces a stable ThemeSnapshot you can bind in any UI stack.

System standout: contrast, scoped brand, color-scheme

Applies CSS color-scheme with data-color-scheme. Use auditThemeContrast / assertThemeContrast, createScopedThemeController, and defineSemanticTokens. Sync document head via bindThemeToHead.

This section is the consumer guide for theming. Preference persistence lives on the Theme store bridge page; the underlying store primitives are documented under Store.

Overview

Sometic theming is behavior first, not a shipped visual kit:

  • You define tokens (color, space, radius, or your own categories).
  • The controller resolves mode, theme id, density, direction, high contrast, and reduced motion into one snapshot.
  • You apply that snapshot to document.documentElement or any scoped element via applyThemeToElement.
  • Your CSS (plain, Tailwind, Bootstrap, or anything else) reads CSS variables and data-* attributes. Sometic does not bundle Tailwind or Bootstrap.

When to use

  • Runtime light / dark / system switching across Vanilla, React, Vue, or other adapters
  • Token → CSS variable pipelines with a stable prefix
  • Scoped themes on a subtree (not only :root)
  • Density, RTL (dir), high-contrast, and reduced-motion flags as attributes
  • Preference persistence across reloads via @sometic/store/persistent

When not to use

  • Class / style / slot resolution only → @sometic/styling
  • Compile-time Figma → code token CI → use Style Dictionary (or similar) and feed results into defineTokens
  • A full visual CSS product as the deliverable → optional presets plus your own CSS
  • Framework-only providers with no shared engine → keep those at the app boundary; this package stays framework-neutral

Package map

SurfaceImportRole
Theme controller@sometic/themecreateThemeController, applyThemeToElement, snapshot types
Tokens@sometic/theme/tokensdefineTokens, mergeTokens, resolveToken
CSS variables@sometic/theme/css-variablestokensToCssVariables, serializeCssVariables
Contrast@sometic/theme/contrastHex parse, luminance, WCAG ratio helpers
System prefs@sometic/theme/systemColor scheme, reduced motion, more contrast (lazy matchMedia)
Presets@sometic/theme/presetsMinimal lightTheme / darkTheme for demos and tests

Presets stay on a subpath so the root controller can stay within its gzip size budget.

Architecture boundary

PackageOwnsDoes not own
@sometic/themeTokens, generation, mode resolution, attributes, contrast helpersClass merging, framework hooks
@sometic/stylingClasses, styles, slots, state attrs, resolveCssVariablesDesign tokens
@sometic/storePreference store, persistence adaptersTheme token math

Theme depends on styling’s resolveCssVariables and on store for subscriptions and optional persistence. Styling never owns tokens.

Quick start

ts
import { createThemeController, applyThemeToElement } 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",
});

applyThemeToElement(document.documentElement, theme.get());

theme.subscribe((snapshot) => {
    applyThemeToElement(document.documentElement, snapshot);
});

theme.setMode("dark");

CSS then consumes variables such as var(--sometic-color-primary) and attributes such as [data-color-scheme="dark"].

Snapshot shape

Every get() / subscribe callback receives a ThemeSnapshot:

FieldType (conceptually)Meaning
preferencesThemePreferencesRaw prefs: mode, themeId, density, direction, highContrast, reducedMotion
resolvedColorScheme"light" | "dark"Effective scheme after system resolution
resolvedThemeIdstringTheme actually used for tokens
tokensThemeTokensActive token map
cssVariablesRecord<string, string>Flat --prefix-category-key map
attributesRecord<string, string>data-theme, data-color-scheme, data-density, dir, optional contrast/motion flags

Docs in this section

PageWhat you will learn
InstallationInstall commands, peers, subpath imports
TokensdefineTokens, merge, resolve paths
ThemesRegister themes, mode vs theme id, presets
Runtime switchingController API, subscribe, dispose, scoped apply
CSS variablesNaming, serialize for SSR, apply cleanup
TailwindMap variables into Tailwind theme extensions
BootstrapDrive Bootstrap-styled UIs from variables and attrs
Plain CSSStyle with variables and state attributes only

Persistence

Pass persist: true with a storage adapter from @sometic/store/persistent. Defaults to in-memory storage unless you pass a web adapter, so durable browser prefs are explicit. Await hydrated before trusting restored values.

Deep dive: Theme store · Store

Contrast honesty

Contrast helpers parse hex colors (#rgb / #rrggbb) today. rgb(), hsl(), and OKLCH strings are not supported yet; invalid input fails closed (meetsWcagContrastfalse, parseHexColorundefined). Treat wider color spaces as a future extension, not current API.

Comparison (short)

ApproachFit
Hand-written :root variablesFine for static sites; Sometic adds registration, system mode, persistence, density/RTL, and a snapshot for any binding
next-themes / React-only providersGreat in Next.js; Sometic stays framework-neutral so Vanilla/Vue/Svelte share one engine
Style DictionaryCompile-time pipelines; use both: generate at build time, switch at runtime with this package