Theming
One base color, a radius, and a text size drive every visual token in Onset. Set them once, and every component reads the same palette from CSS variables.
Theme configuration accepts five base inputs: baseColor (any hex), radiusBase (px number), fontSizeBase (px number), spacingBase (px number), and mode ("light" | "dark"). baseColor is the only color input: it drives the accent scale and tints the neutral ramp, so every surface, border, and text color is derived from a single hex value.
The default theme is dark with a purple accent (#7366ff). Configure it through ThemeProvider props. Nested providers scope overrides to a subtree.
import { ThemeProvider } from "@solarius/onset";
import "@solarius/onset/styles.css";
export default function App() {
return (
<ThemeProvider defaultBaseColor="#e67e22" defaultMode="dark">
{/* Every component here reads from the warm palette */}
</ThemeProvider>
);
}Theme inputs
Every visual property in Onset is computed from five inputs:
baseColor
A hex color like #7366ff, #e74c3c, or #2ecc71. Its hue tints the 12-step neutral surface ramp, so backgrounds and text carry some of the brand hue. Its full HSL drives the 12-step accent scale, which becomes the primary action color, hover states, and bright accent text. Status colors (ok, warn, err) are not derived from it: ok is green, warn is amber, err is red in every theme. They shift with mode, and can be overridden through the status prop; see Status colors below.
radiusBase
A pixel number (default 8) that every corner radius is a multiple of: xs = 0.5×, sm = 0.75×, md = 1×, lg = 1.25×, xl = 2×. Set to 0 for sharp corners, 12 for rounder UI.
fontSizeBase
A pixel number (default 13) that anchors the type scale. Every other step, nano through display, keeps a fixed proportion to it, so raising the base scales headings and labels with the body text. Set 11 for a dense console, 15 for a roomier app.
Control heights (--m-control-height-*) and spacing are not derived from it, so changing the text size does not reflow layouts.
Prose follows it too. Long-form text runs a step above the console density, and its headings are em multiples of that body size, so that scale also moves with the base.
spacingBase
A pixel number (default 4) that every --m-space-* step is a multiple of: space-2 is two units, space-0_5 is half of one. It sets the spacing density. Set 3 for tighter console spacing, 5 or 6 for more room.
--m-space-px stays 1px at every density: it is a border that exists to match a 1px border, not a spacing step.
mode
"light" or "dark". Controls which ramp shape is used for neutral and accent steps. The neutral ramp inverts: dark mode goes from deep chromes to bright text; light mode goes from bright surfaces to dark text.
Status colors
ok, warn and err are the only palette entries not derived from baseColor (see above). Pass status to override them; anything you omit keeps the mode's default:
<ThemeProvider status={{ ok: "#3fb950", err: "#f85149" }}>
{/* warn keeps the mode's default amber */}
</ThemeProvider>They land on --m-color-ok / --m-color-warn / --m-color-err. Tone-carrying components read these through --m-tone-base, so a Banner, a StatusDot, a ProgressBar and a chart series all use the same values. On the token side the same thing is buildPalette(hex, mode, { ok, warn, err }).
Custom fonts
Pass fonts to replace Onset’s sans and/or monospace family stack. Every component reads --m-font-sans or --m-font-mono, so the change applies to controls, prose, code, charts, and overlays inside the provider.
import "@fontsource/inter";
import "@fontsource/jetbrains-mono";
<ThemeProvider
fonts={{
sans: '"Inter", system-ui, sans-serif',
mono: '"JetBrains Mono", ui-monospace, monospace',
}}
>
<YourApp />
</ThemeProvider>;You may override only one family; the omitted family keeps Onset’s default. ThemeProvider writes the CSS family names but does not fetch font files, so load them through @font-face, your framework’s font loader, or a font package first. Nested providers can use different stacks for a scoped subtree.
ThemeProvider
ThemeProvider resolves the theme inputs into the full --m-* CSS variable set and writes them onto a wrapper element. Only components rendered inside the provider read those variables.
In uncontrolled mode, pass defaultBaseColor, defaultRadiusBase, defaultFontSizeBase, and defaultMode to set initial values, then use useTheme() to change them at runtime:
<ThemeProvider defaultBaseColor="#3498db" defaultMode="dark">
<YourApp />
</ThemeProvider>You can nest ThemeProviders to scope different themes to parts of a page. Each nested provider writes its own --m-* variables and overrides the parent’s for its subtree. This is how the playground’s live preview pane works.
The as prop changes the wrapper element (default "div"). Set renderWrapper=false to skip the wrapper and rely on variables already present on an ancestor; rarely needed outside testing.
Controlled mode
When you need to drive the theme from external state (a theme picker, a settings panel, or URL query parameters), pass the controlled props along with their change handlers:
import { useState } from "react";
import { ThemeProvider } from "@solarius/onset";
export default function App() {
const [baseColor, setBaseColor] = useState("#7366ff");
const [mode, setMode] = useState<"light" | "dark">("dark");
const [radiusBase, setRadiusBase] = useState(8);
const [fontSizeBase, setFontSizeBase] = useState(13);
return (
<ThemeProvider
baseColor={baseColor}
onBaseColorChange={setBaseColor}
mode={mode}
onModeChange={setMode}
radiusBase={radiusBase}
onRadiusBaseChange={setRadiusBase}
fontSizeBase={fontSizeBase}
onFontSizeBaseChange={setFontSizeBase}
>
<YourApp />
</ThemeProvider>
);
}In controlled mode, defaultBaseColor / defaultRadiusBase / defaultFontSizeBase / defaultMode are ignored; the controlled props take precedence.
useTheme hook
useTheme() returns the current theme context: { theme, config, setBaseColor, setRadiusBase, setFontSizeBase, setMode }.
theme is the ResolvedTheme object containing the full palette, radius scale, font-size scale, and config. config is the current ThemeConfig ( baseColor, radiusBase, fontSizeBase, spacingBase, mode). The setters update configuration programmatically and work in both controlled and uncontrolled mode.
import { useTheme } from "@solarius/onset";
function ThemeSwitcher() {
const { config, setBaseColor, setMode } = useTheme();
return (
<div>
<p>
Current: {config.baseColor} ({config.mode})
</p>
<button onClick={() => setBaseColor("#e74c3c")}>Red</button>
<button onClick={() => setBaseColor("#2ecc71")}>Green</button>
<button
onClick={() => setMode(config.mode === "dark" ? "light" : "dark")}
>
Toggle mode
</button>
</div>
);
}CSS variables
The resolved theme produces the full --m-* CSS variable set. All Onset components read from these variables. You can read them in your own CSS too:
DEFAULT_THEME
The DEFAULT_THEME constant exports the reference config:
import { DEFAULT_THEME } from "@solarius/onset";
console.log(DEFAULT_THEME);
// {
// baseColor: "#7366ff",
// radiusBase: 8,
// fontSizeBase: 13,
// fonts: {
// sans: '"IBM Plex Sans", "Segoe UI", Arial, sans-serif',
// mono: '"IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace',
// },
// spacingBase: 4,
// mode: "dark",
// }You can pass a partial config to resolveTheme() to get the full ResolvedTheme without mounting a provider. Call themeToCssVars() on the result to get the flat --m-* map, useful for build-time CSS generation or server-side rendering.