Solid
Signals-first theming with a context provider and scoped subtrees.
@themvora/solidSolidv2.0.0StableSSR:NoZero-flash:Partial1Install
pnpm add @themvora/core @themvora/solid2Set up
// The Vite entry: render the tree and load the stylesheet once.
import "./index.css";
import { App } from "./App";
import { ThemeProvider } from "@themvora/solid";
import { render } from "solid-js/web";
render(
() => (
<ThemeProvider defaultTheme="light" initialMode="system">
<App />
</ThemeProvider>
),
document.getElementById("root")!,
);defaultTheme chooses the starting theme. Add initialMode="system" to follow the visitor's OS on the first visit.3Add a small UI
Design system
ActiveTokens resolve at runtime, so this card follows whichever theme is active.
export function WorkspaceCard() {
return (
<div class="themvora-example-card">
<div class="themvora-example-header">
<h3 class="themvora-example-title">Design system</h3>
<span class="themvora-example-status">Active</span>
</div>
<p class="themvora-example-description">
Tokens resolve at runtime, so this card follows whichever theme is active.
</p>
<p class="themvora-example-meta">Updated 2 minutes ago</p>
<button class="themvora-example-button" type="button">View themes</button>
</div>
);
}4Style it with Themvora tokens
/* Themvora emits its design tokens as CSS custom properties on the root
element, so a stylesheet only has to read them. Nothing here is
theme-specific: these rules render every theme, including ones you add
later. */
html,
body {
background: var(--theme-color-background);
color: var(--theme-color-foreground);
}
.themvora-example-card {
display: grid;
gap: 10px;
max-width: 22rem;
padding: 16px;
border: 1px solid var(--theme-color-border);
border-radius: var(--theme-radius-lg, 12px);
background: var(--theme-color-card);
color: var(--theme-color-cardForeground);
font-family: inherit;
line-height: 1.5;
box-shadow: var(--theme-shadow-sm, 0 1px 2px rgb(0 0 0 / 0.06));
}
.themvora-example-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.themvora-example-title {
margin: 0;
font-size: 15px;
font-weight: 600;
}
.themvora-example-status {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 500;
color: var(--theme-color-success);
}
.themvora-example-status::before {
content: "";
width: 6px;
height: 6px;
border-radius: 9999px;
background: currentColor;
}
.themvora-example-description {
margin: 0;
font-size: 13px;
color: var(--theme-color-mutedForeground);
}
.themvora-example-meta {
margin: 0;
font-size: 12px;
color: var(--theme-color-mutedForeground);
}
.themvora-example-button {
justify-self: start;
padding: 8px 14px;
border: 0;
/* Presets and generated themes emit --theme-radius-lg and nothing else on
the radius scale, so the smaller step is derived from it rather than
assumed — the same proportion the utility-class mapping uses. */
border-radius: calc(var(--theme-radius-lg, 12px) * 0.8);
background: var(--theme-color-primary);
color: var(--theme-color-primaryForeground);
font: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.themvora-example-button:hover {
filter: brightness(1.08);
}
.themvora-example-button:focus-visible {
outline: 2px solid var(--theme-color-ring);
outline-offset: 2px;
}.themvora-example-card and its children read Themvora tokens, so switching the active theme repaints them — no re-render, and no per-theme CSS.The `body` rule uses exactly the two foundational colours — `background` and `foreground`. Every component rule reads one of the 21 colours `validateTheme()` requires, so any valid theme emits it. Radius and shadow are optional and carry fallbacks; no typography token is read, so a theme without a typography group still renders correctly.
5Run and choose a theme
import { useTheme } from "@themvora/solid";
const page = {
background: "var(--theme-color-background)",
color: "var(--theme-color-foreground)",
"min-height": "100vh",
padding: "2rem",
};
export function App() {
// Signals: call them to read the current value.
const { theme, mode, toggleTheme } = useTheme();
return (
<main style={page}>
<h1>Themvora</h1>
<p>
Current theme: {theme().name} ({mode()})
</p>
<button
onClick={toggleTheme}
style={{
background: "var(--theme-color-primary)",
color: "var(--theme-color-primaryForeground)",
border: 0,
"border-radius": "0.5rem",
padding: "0.5rem 1rem",
}}
>
Toggle theme
</button>
<section
style={{
background: "var(--theme-color-card)",
border: "1px solid var(--theme-color-border)",
"border-radius": "0.75rem",
"margin-top": "1.5rem",
padding: "1rem",
}}
>
<h2>Themed card</h2>
<p style={{ color: "var(--theme-color-mutedForeground)" }}>
Surface, border and text follow the theme.
</p>
</section>
</main>
);
}Theme configuration
import { render } from "solid-js/web";
import { ThemeProvider } from "@themvora/solid";
import { App } from "./App";
render(
() => (
<ThemeProvider defaultTheme="mint-light">
<App />
</ThemeProvider>
),
document.getElementById("root")!,
);theme.config.ts instead and let themvoraVitePlugin() derive the pre-paint bootstrap from the same file — the provider then takes no theme props. The plugin is optional either way.What's Available
@themvora/solid ships 16 exports in 5 categories. Click an export to see what it does and jump to the code that uses it.
Three pages cover this package, and they do different jobs: this index says what exists, the examples page shows each export in code with the problem it solves, and the API reference gives exact signatures and return types.
Use Cases
Short snippets you can drop into an existing app. For the same problems worked through end to end — problem, APIs, expectations, limitations — see the Solid examples.
Toggle light / dark
useTheme returns signals and getters for fine-grained reactivity.
import { useTheme } from "@themvora/solid";
function ThemeToggle() {
const { theme, mode, setMode, toggleTheme } = useTheme();
return (
<div>
<button onClick={() => toggleTheme()}>{theme().name}</button>
<button onClick={() => setMode("dark")}>Dark</button>
<span>{mode()}</span>
</div>
);
}Undo / redo theme changes
History controls as signals, plus live runtime updates.
import { useThemeHistory, useThemeRuntime } from "@themvora/solid";
function HistoryControls() {
const { undo, redo, canUndo, canRedo } = useThemeHistory();
const runtime = useThemeRuntime();
return (
<div>
<button onClick={() => undo()} disabled={!canUndo()}>Undo</button>
<button onClick={() => redo()} disabled={!canRedo()}>Redo</button>
<button onClick={() => runtime.update({ colors: { primary: "#6366f1" } })}>
Make primary indigo
</button>
</div>
);
}Scope a subtree
ThemeScope themes a subtree with scoped CSS variables.
import { ThemeScope } from "@themvora/solid";
function Dashboard() {
return (
<>
<Sidebar />
<ThemeScope theme="plum-dark">
<DataViz />
</ThemeScope>
</>
);
}Lifecycle events
Subscribe to lifecycle events and install theme packs.
import { onMount } from "solid-js";
import { useThemeLifecycle, useThemePacks } from "@themvora/solid";
import { getAccessibilityProfiles } from "@themvora/core";
import type { ThemeLifecycleEventMap } from "@themvora/core";
function Telemetry() {
const { on } = useThemeLifecycle();
const applyPack = useThemePacks();
onMount(() =>
on("beforeThemeChange", (e) => {
const { next } = e as ThemeLifecycleEventMap["beforeThemeChange"];
console.log("theme changing to", next.name);
}),
);
return (
<button onClick={() => applyPack({ name: "a11y", themes: getAccessibilityProfiles() })}>
Apply High Contrast pack
</button>
);
}Smooth theme transitions
Enable CSS transitions on theme changes for a polished user experience.
import { ThemeProvider } from "@themvora/solid";
export function App() {
return (
<ThemeProvider
transition={{ enabled: true, duration: 300, easing: "ease-in-out" }}
>
<YourApp />
</ThemeProvider>
);
}Scope and history in one component
The same pieces as the other integrations, with Solid's rule attached: read accessors where you want a subscription, and keep the history object intact.
import { ThemeProvider, ThemeScope, useThemeHistory } from "@themvora/solid";
function HistoryControls() {
// `canUndo` / `canRedo` are getters — read them off the object in JSX.
// Destructuring freezes the mount-time value and the buttons never enable.
const history = useThemeHistory();
return (
<div>
<button onClick={history.undo} disabled={!history.canUndo}>Undo</button>
<button onClick={history.redo} disabled={!history.canRedo}>Redo</button>
</div>
);
}
export function App() {
return (
<ThemeProvider defaultTheme="mint-light">
<ThemeScope theme="plum-dark" transition={{ duration: 300, easing: "ease" }}>
<span>Always plum-dark</span>
</ThemeScope>
<HistoryControls />
</ThemeProvider>
);
}history.canUndo is read inside JSX so it tracks. Destructuring it would freeze the mount-time value and the button would never enable.
The complete runnable app behind this guide is examples/apps/solid — listed with the other example apps.