Quickstart
Quick Start
The shortest path to a working light/dark theme. No token definitions, no configuration — install, register the runtime, and toggle.
Pick your framework
Themvora ships a built-in neutral theme, so you can skip defining tokens entirely and still get a complete, toggleable light/dark theme. These snippets are the whole setup.
1 · Install
Install the core package and the adapter for your framework.
pnpm add @themvora/core @themvora/next2 · Set up
The ThemeProvider in @themvora/next is a server component. It reads the theme cookies during the request and renders <html data-theme> with the resolved variables already inline, which is what makes the first byte correct.
import type { ReactNode } from "react";
import { ThemeProvider } from "@themvora/next";
import "./globals.css";
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<ThemeProvider defaultTheme="light">
{children}
</ThemeProvider>
);
}The server resolves the mode from the persisted cookies, so a first-time visitor starts on defaultTheme. Once they pick System, that choice is stored and reused on every later request.
Themes and the starting mode come from the provider in app/layout.tsx. Next does not use Vite, so there is no bundler plugin to add: the provider resolves the selection on the server and emits the pre-paint script itself.
3 · Add a small UI
A small status card with a heading, a description, a status label and a primary button. The class names are ordinary CSS; the next step connects them to the theme.
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>
);
}4 · Style it with Themvora tokens
Your own classes read the variables the active theme emits, so there is no per-theme CSS to write and nothing to re-render on a switch. The document rule uses only the two required foundational colours; the component rules use only required semantic colours, so no typography token is involved.
Design system
ActiveTokens resolve at runtime, so this card follows whichever theme is active.
/* 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 stylesheet is identical in every framework, and it is where the theme actually reaches the component. It reads ten required colour tokens — background, foreground, card, cardForeground, border, mutedForeground, success, primary, primaryForeground and ring — all of them required, so every theme that passes validateTheme() emits them. The two optional tokens it touches, radius and shadow, are read through a fallback so a theme without them still lays out correctly. It reads no typography token at all: the font is inherited, and the line height is an ordinary value, because a valid theme may define no typography.
5 · Run and choose a theme
The control below reads and writes the runtime. Switch between the supplied families — or a custom theme with the required colours — and the card from step 3 repaints.
"use client";
import { useTheme } from "@themvora/next/client";
export function ThemeSwitcher() {
const { theme, mode, toggleTheme } = useTheme();
return (
<main
style={{
background: "var(--theme-color-background)",
color: "var(--theme-color-foreground)",
minHeight: "100vh",
padding: "2rem",
}}
>
<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,
borderRadius: "0.5rem",
padding: "0.5rem 1rem",
}}
>
Toggle theme
</button>
<section
style={{
background: "var(--theme-color-card)",
border: "1px solid var(--theme-color-border)",
borderRadius: "0.75rem",
marginTop: "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>
);
}You're ready
You have a provider, a small UI and a stylesheet that reads only the token contract every valid theme guarantees. Choose another supplied family — or pass a custom theme with the 21 required colours and nothing else — and the same card repaints.
Next steps
Ready for real themes?
The built-in themes cover the full semantic token set, so staying on them is a valid choice. When you want your own palette, define a theme and pass it in — the built-ins stay available alongside it.
// The provider falls back to these two when you pass no `themes` prop:
// the neutral "light" and "dark" themes.
import { getBuiltInThemes, getNeutralThemes } from "@themvora/core";
const [light, dark] = getNeutralThemes();
// The full built-in set is 34 themes — 14 color families plus the neutral,
// high-contrast and large-text pairs. Merge it with your own. Theme names must
// be unique, so don't redefine a built-in name.
const themes = [...getBuiltInThemes(), ...myThemes];