React
Provider and hooks for React, with scoped themes, transitions, and an optional build-time bootstrap.
@themvora/reactReactv2.0.0StableSSR:NoZero-flash:PartialWhich React setup should I use?
Use <ThemeProvider>Use createThemeRoot()Use the framework integration1Install
pnpm add @themvora/core @themvora/react2Set up
import { createRoot } from "react-dom/client";
import { ThemeProvider } from "@themvora/react";
import "./index.css";
import { App } from "./App";
createRoot(document.getElementById("root")!).render(
<ThemeProvider defaultTheme="light" initialMode="system">
<App />
</ThemeProvider>,
);// Application UI only; the provider lives in src/main.tsx.
import { ThemeSwitcher } from "./ThemeSwitcher";
import { WorkspaceCard } from "./components/WorkspaceCard";
export function App() {
return (
<main>
<ThemeSwitcher />
<WorkspaceCard />
</main>
);
}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 className="themvora-example-card">
<div className="themvora-example-header">
<h3 className="themvora-example-title">Design system</h3>
<span className="themvora-example-status">Active</span>
</div>
<p className="themvora-example-description">
Tokens resolve at runtime, so this card follows whichever theme is active.
</p>
<p className="themvora-example-meta">Updated 2 minutes ago</p>
<button className="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/react";
export function ThemeSwitcher() {
const { theme, toggleTheme } = useTheme();
return (
<button type="button" onClick={toggleTheme}>
Theme: {theme.name} — toggle
</button>
);
}Theme configuration
import { createRoot } from "react-dom/client";
import { ThemeProvider } from "@themvora/react";
import "./index.css";
import { App } from "./App";
createRoot(document.getElementById("root")!).render(
<ThemeProvider
defaultTheme="mint-light"
initialMode="system"
transition={{ enabled: true, duration: 250 }}
>
<App />
</ThemeProvider>,
);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/react ships 30 exports in 6 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 React examples.
Toggle light / dark
Read the active theme, mode and family and flip between them with zero config.
import { useTheme } from "@themvora/react";
export function ThemeToggle() {
const { theme, mode, family, setMode, setFamily, toggleTheme } = useTheme();
return (
<div className="row">
<button onClick={toggleTheme}>{theme.name}</button>
<button onClick={() => setMode("dark")}>Dark</button>
<button onClick={() => setMode("light")}>Light</button>
<select value={family} onChange={(e) => setFamily(e.target.value)}>
{["default", "mint", "plum"].map((f) => (
<option key={f} value={f}>{f}</option>
))}
</select>
<span className="mono">{mode}</span>
</div>
);
}Undo / redo + live token editing
Theme changes are snapshotted automatically. Undo, redo, or patch tokens at runtime.
import { useThemeHistory, useThemeRuntime } from "@themvora/react";
export function ThemeControls() {
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
Apply a specific theme to a section of the tree with scoped CSS variables.
import { useRef } from "react";
import { ThemeScope, useScopedTheme } from "@themvora/react";
import type { ThemeTransitionOptions } from "@themvora/core";
const transition: ThemeTransitionOptions = { duration: 300, easing: "ease" };
export function Dashboard() {
return (
<>
<Sidebar /> {/* inherits the global theme */}
<ThemeScope theme="plum-dark" transition={transition}>
<DataViz /> {/* always themed "plum-dark" */}
</ThemeScope>
</>
);
}
export function ScopedCard() {
const ref = useRef<HTMLDivElement>(null);
useScopedTheme(ref, "plum-dark");
return <div ref={ref}>Imperatively scoped</div>;
}Lifecycle events + theme packs
React to every theme change, and install a ready-made pack (e.g. a11y profiles) at runtime.
import { useEffect } from "react";
import { useThemeLifecycle, useThemePacks } from "@themvora/react";
import { getAccessibilityProfiles } from "@themvora/core";
export function Telemetry() {
const { on } = useThemeLifecycle();
const applyPack = useThemePacks();
useEffect(() => {
const off = on("beforeThemeChange", (e) => {
console.log("theme changing to", e.next.name);
});
return off;
}, [on]);
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 { createRoot } from "react-dom/client";
import { ThemeProvider } from "@themvora/react";
// Your application root. The provider lives here; App only renders UI.
function App() {
return <main>Your app</main>;
}
createRoot(document.getElementById("root")!).render(
<ThemeProvider
transition={{
enabled: true,
duration: 300,
easing: "ease-in-out",
properties: [
"color",
"background-color",
"border-color",
"border-radius",
"font-size",
"box-shadow",
],
}}
>
<App />
</ThemeProvider>,
);A scoped panel, and the history behind it
ThemeScope applies a theme to one subtree, and useThemeHistory() drives undo and redo. Both are local to the app — neither changes what the rest of the page renders.
import { ThemeScope, useThemeHistory } from "@themvora/react";
function HistoryControls() {
const { undo, redo, canUndo, canRedo } = useThemeHistory();
return (
<div>
<button onClick={undo} disabled={!canUndo}>Undo</button>
<button onClick={redo} disabled={!canRedo}>Redo</button>
</div>
);
}
export function ThemeScopePanel() {
return (
<>
{/* Pinned to plum-dark regardless of the app-wide selection. */}
<ThemeScope theme="plum-dark" transition={{ duration: 300, easing: "ease" }}>
<p>This subtree is always plum-dark.</p>
</ThemeScope>
<HistoryControls />
</>
);
}A plum-dark panel inside a mint-light app, with undo and redo wired to the history the provider keeps. The provider already lives in src/main.tsx, so this component only consumes it.
The complete runnable app behind this guide is examples/apps/react — listed with the other example apps.
Optimized CSR bootstrap
React's createRoot().render() may schedule the initial commit concurrently. In client-only applications where a delayed first commit creates a visible startup jerk, Themvora provides createThemeRoot() as an opt-in root bootstrap. Most applications should continue using <ThemeProvider> directly — this is a specialized tool, not a requirement.
import { createThemeRoot } from "@themvora/react";
import "@themvora/core/scrollbar.css";
import { App } from "./App";
createThemeRoot({
container: document.getElementById("root")!,
defaultTheme: "mint-light",
initialMode: "system",
transition: { enabled: true },
render: () => <App />,
});ThemeProvider is the normal React integration. createThemeRoot() is an optional root-level bootstrap that owns the root boundary.When does createThemeRoot() matter?
Use it when you have measured a visible first-render scheduling gap in a client-only React application. It is an opt-in behavior, not a requirement for ordinary Themvora usage.
Architecture
Normal React (recommended for most apps):
Optimized CSR bootstrap:
SSR frameworks:
Composition stays yours
createThemeRoot() does not own application composition. It owns the root; the render({ runtime }) callback owns the tree, so arbitrary providers (MUI, Chakra, React Query, Redux, Router, …) compose freely and can derive their configuration from the Themvora runtime:
// main.tsx — composition stays fully in your hands
import { createThemeRoot } from "@themvora/react";
import { MuiThemeProvider } from "@themvora/mui";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { themes } from "./themes";
import App from "./App";
const queryClient = new QueryClient();
createThemeRoot({
container: document.getElementById("root")!,
themes,
defaultTheme: "mint-light",
initialMode: "system",
render: ({ runtime }) => (
<MuiThemeProvider runtime={runtime}>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</MuiThemeProvider>
),
});Why doesn't ThemeProvider call flushSync()?
ThemeProvider lives inside the React root. It cannot change how the owning root was scheduled, and calling flushSync() from inside the provider would couple the component to React's root scheduling semantics. createThemeRoot() exists specifically so the root-level behavior can be controlled at the correct boundary.
createThemeRoot() is intended for client-rendered React applications. Framework integrations such as Next.js and Remix own their application root/hydration lifecycle and should use their framework-specific Themvora integration instead.