Vesture
Get started

Concepts

Theming

Every component in Vesture is written against a single contract: vars, from @vesture/tokens. No component file references a hex code, a pixel value, or a font stack directly — it references vars.color.primary, vars.space.md, vars.font.display, and so on. A theme is just a concrete value for every slot in that contract.

See it in action

This panel is real @vesture/react — the same components documented throughout this site. Toggling the theme here also toggles the theme used in every live demo on the component reference pages, using the same toggle in the header.

Rendering in the default theme — same components, same code, zero component-level changes.

Jane DoeProduct designer
Online
Task completed72%

The contract

@vesture/tokens defines the contract with vanilla-extract's createThemeContract — color, space, radius, shadow, border, font, and motion slots, each just a name with no value:

contract.css.ts
export const vars = createThemeContract({
  color: { background: null, surface: null, primary: null, /* … */ },
  space: { xs: null, sm: null, md: null, lg: null, xl: null, "2xl": null },
  radius: { none: null, sm: null, md: null, lg: null, full: null },
  shadow: { none: null, sm: null, md: null, lg: null, focus: null },
  font: { display: null, body: null, mono: null, /* size/weight/line-height */ },
  motion: { durationFast: null, durationNormal: null, easing: null }
});

Implementing a theme

@vesture/theme-retro is a real second theme, built by a different maintainer than the components themselves, using nothing but that contract:

retro-theme.css.ts
import { createTheme } from "@vanilla-extract/css";
import { vars } from "@vesture/tokens";

export const retroThemeClass = createTheme(vars, {
  color: {
    background: "#faf3e8",
    surface: "#f2e6d3",
    text: "#3b2a1a",
    primary: "#c1502e",
    // …every slot in the contract needs a value
  },
  // space, radius, shadow, font, motion…
});

TypeScript enforces completeness — createThemewon't compile until every slot the contract declares has a value, so a new theme can't accidentally leave a component unstyled.

Applying a theme

Put the theme's class on <html>, not an inner wrapper. Modal, Tooltip, Popover, and DropdownMenu render through a React portal straight into document.body— CSS custom properties only cascade down the DOM tree they're set on, so a class on a wrapper <div> never reaches those portaled nodes. The class on <html> does, because portaled content still lives inside <html>.

tsx
import { defaultThemeClass } from "@vesture/tokens";
// or: import { retroThemeClass } from "@vesture/theme-retro";

<html className={defaultThemeClass}>
  <body>{children}</body>
</html>

Runtime theme switching

Because a theme is just a class name, switching themes at runtime — a light/dark toggle, a per-tenant brand theme, a user preference — is a single classList swap, no re-render of the component tree required:

tsx
function toggleTheme(next: "default" | "retro") {
  document.documentElement.classList.remove(defaultThemeClass, retroThemeClass);
  document.documentElement.classList.add(next === "default" ? defaultThemeClass : retroThemeClass);
}