Theming
How ViraUI themes work—tokens, CSS, JSON, built-in presets, and custom brands.
A theme is the visual substrate your app loads once: design tokens compiled into CSS custom properties (and optional JSON/types for tooling). Swap the theme and the same @viraui/react components pick up a different brand. For packages and root wiring, see Setup. Token facets (color, type, space, …) live under Foundation.
What is a theme
Every theme ships the same kinds of surfaces. You consume them as CSS imports—not as a folder layout.
- Tokens — Shared values for color, type, space, radius, motion, elevation-related effects, and more. Browse facets in Foundation.
- Theme CSS — One stylesheet of CSS custom properties. Load it before fonts and
@viraui/react/preflight.css. That file is the runtime look of your UI. - JSON — Flat and nested dumps of the same tokens for agents, codegen, and sync tooling. Optional at runtime.
- Types — TypeScript names for token keys when you reference foundation values in typed code.
Light and dark
Light and dark are not separate themes, and you do not load a second “dark.css”. They live inside the same theme stylesheet as paired values. Built-in presets express that pairing with the CSS light-dark() function: each semantic token is defined once, with a light value and a dark value side by side—for example --global-background: light-dark(…light…, …dark…). When the browser’s color-scheme is light, the first argument wins; when it is dark, the second does.
Preflight is what connects that to your app. It maps data-mode on the document root (or a wrapping element) to color-scheme, so flipping the attribute is enough for every paired token to resolve to the matching side—without swapping stylesheets or duplicating selectors.
<html lang="en" data-mode="light">Use light, dark, or inverted as needed. Prefer following the user’s system preference? That wiring is yours: read prefers-color-scheme (or your own store) and set data-mode to light or dark. Mode is independent of which brand CSS you imported—brand is the theme file; mode is which scheme that file resolves to.
When you author a custom theme, prefer the same light-dark() pattern so data-mode flips keep working from a single sheet. Put pairs on the tokens that actually change with scheme—especially --global-*, --highlight-*, and --base-*. Mode-invariant tokens (space, radius, type scale, motion) can stay as plain values. App Studio exports already follow this convention; if you hand-edit or ask an agent to generate CSS, keep the pairs on those semantic names.
Other pairing strategies are fine—selectors under [data-mode='dark'], media queries, and so on—as long as every semantic token your components expect still resolves in each mode you support. What you should not do is treat a second dark stylesheet as the brand swap: that confuses mode with theme.
@viraui/foundation
pnpm add @viraui/foundation@viraui/foundation is the optional package that ships the built-in themes: CSS for the browser, plus JSON and TypeScript surfaces for each preset. It does not include font files—add Fontsource (or your own font loading) separately for the theme you pick.
- Add it when you use Vira, Vira Condensed, Sunburst, or Cinder.
- Skip it when you ship your own theme CSS (Studio export or hand-authored). Components only need a compatible theme stylesheet—not this package.
@viraui/react does not pull foundation or fonts in for you. Wire theme CSS and Fontsource at the app root yourself, or let viraui-setup do it during Setup.
Built-in themes
Four ready presets. Each is a single CSS entry. Install the matching Fontsource packages and import those stylesheets after the theme CSS, before preflight. Theme tokens only name the families; they do not ship the files.
Load one theme CSS. Switching brands means changing that import and installing the matching Fontsource packages—not stacking several theme files.
pnpm add @fontsource-variable/geist @fontsource-variable/geist-monoimport '@viraui/foundation/vira.css';
import '@fontsource-variable/geist/wght.css';
import '@fontsource-variable/geist-mono/wght.css';
import '@viraui/react/preflight.css';Custom themes
A custom brand is the same contract as a preset: one theme CSS file that keeps the semantic variable names and swaps in your values. You do not need @viraui/foundation for this—load your sheet, then the fonts named in --font-family-*, then preflight. Keep light and dark in that same file with light-dark() on scheme-sensitive tokens; Light and dark covers the pairing pattern.
import './vira-theme.css';
import '@fontsource-variable/geist/wght.css'; // match families in your theme
import '@viraui/react/preflight.css';:root {
--global-background: light-dark(oklch(98% 0.01 250), oklch(18% 0.02 250));
--global-foreground: light-dark(oklch(22% 0.02 250), oklch(96% 0.01 250));
--global-primary: light-dark(oklch(55% 0.14 250), oklch(72% 0.12 250));
/* space, radius, type scale… can stay unpaired */
}App Studio (ViraUI Pro)
Hand-editing a full theme means forking a large CSS dump, keeping every semantic name stable, balancing light/dark pairs, and checking that components still look right. App Studio in ViraUI Pro removes that grind: tune the brand on a live ViraUI canvas, share the theme with the team, export CSS that is already compatible—then drop it into the app.
Typical Studio path takes minutes, not a token archaeology session:
Shape the brand live
Tune colors, type, space, and effects in Studio while ViraUI components update on the canvas.
Export and save
Export theme.css (ZIP entry or paste) and save it in the app—for example src/vira-theme.css.
Wire fonts
Install @fontsource-variable/* for the families in --font-family-*. Strip temporary Studio CDN @font-face blocks once npm fonts are wired.
Import once
Import theme → fonts → preflight. Do not also install @viraui/foundation.
For a new brand, prefer Studio. Reach for an agent-built sheet or a tiny :root override only when Studio is not an option.
Coming later this year
ViraUI Pro sits on top of Core. Join the waitlist on viraui.dev for launch pricing.
Ask an agent
No Pro yet? Ask your agent to create a ViraUI-compatible theme for your brand and wire it like any other custom sheet (theme → fonts → preflight). Describe the product look in plain language—the agent should produce a complete theme CSS with light-dark() pairs on scheme-sensitive tokens, not a partial token dump. For the fastest, designer-ready path, use App Studio above.
Load a complete theme
Components expect every semantic token from the theme contract—not a handful of overrides alone. Ship a full theme CSS (Studio export, agent-built sheet, or a built-in preset), or import a base theme first and only then layer partial :root overrides. A partial sheet with no base theme leaves missing variables and broken UI—do not do that.
Only after a complete theme is already loaded, you may layer a few semantic overrides:
import '@viraui/foundation/vira.css'; // or your full custom sheet
import './brand-overrides.css'; // only the tokens you change
import '@fontsource-variable/geist/wght.css';
import '@viraui/react/preflight.css';:root {
/* Prefer light-dark() so data-mode flips keep working */
--global-primary: light-dark(oklch(55% 0.14 250), oklch(72% 0.12 250));
--space-medium: 0.875rem;
}Scoped wrappers work the same way when only a subtree should rebrand. Full brand swaps = replace the complete theme CSS import, not data-mode.
Last updated on