Theming
Swap the whole app's look by changing the preset — a fixed vocabulary of semantic tokens maps to your design.How theming works
Theming has two axes that stay cleanly separated:
- Runtime —
<ThemeProvider preset={…}>hands components their recipes at render time. - Config — the preset's stylesheets define, at build time, the utilities that read the
--hope-*tokens (@import "@hope-ui/presets/hope/tailwind.css") and their values (@import "@hope-ui/presets/hope/theme.css"— opt-out, so you can supply your own).
@hope-ui/theming is the seam between the two: components read from it, presets implement it, and
neither imports the other. That's what lets you restyle everything without touching component code.
Presets
A preset is a complete visual identity. hope is the default — a clean, modern baseline. To
change the whole app's look, pass a different preset to ThemeProvider; your component markup
doesn't change.
import { ThemeProvider } from "@hope-ui/theming";
import { hope } from "@hope-ui/presets/hope";
<ThemeProvider preset={hope}>
{/* every component below inherits this preset */}
</ThemeProvider>;Semantic color tokens
Every hope-ui component paints from one fixed vocabulary of semantic tokens — never a raw color. Color is organized into six roles, each a meaning rather than a hue:
primary
neutral
success
info
warning
danger
Each role expands into a small, predictable set of modifiers, all exposed as Tailwind utilities
({role} is any of the six above):
| Utility | What it paints |
|---|---|
bg-{role} | The solid role fill (e.g. bg-primary). |
text-on-{role} | Text/icon color that stays legible on the solid fill. |
bg-{role}-soft | The tonal, low-emphasis fill. |
text-{role}-emphasis | Role-colored text on a neutral surface. |
border-{role}-line | The role's border color. |
Alongside the roles, a neutral vocabulary covers surfaces, text, borders, and system colors:
| Utility | What it paints |
|---|---|
bg-surface / -raised / -sunken / -inverse | Elevation — the page, a raised card, a sunken well, an inverse surface. |
text-foreground / -muted / -subtle | The neutral text ramp, strongest to faintest. |
border-subtle / -strong | Neutral borders and dividers. |
ring-focus | The focus ring. |
Because everything reads from this set, a solid primary button is literally
bg-primary text-on-primary hover:bg-primary-hovered — repoint the token and every use follows.
Customizing
Component defaults & part classes
Set defaults and global part classes for every instance of a component with definePreset, then
pass the derived preset to ThemeProvider. defaultProps resolve at instance ?? preset ??
builtin precedence, so an explicit prop on one instance always wins.
import { definePreset } from "@hope-ui/theming";
import { hope } from "@hope-ui/presets/hope";
export const myPreset = definePreset(hope, {
components: {
button: {
// Every recipe variant is defaultable.
defaultProps: { size: "lg" },
// Global part classes, folded in before per-instance slotClasses.
slotClasses: { root: "rounded-full" },
},
},
});Each component's page documents which props are defaultable and which parts it exposes — see Button for a worked example.
Token values
theme.css with every token filled in.To change the actual colors, author your own --hope-* CSS. Redefine any token under :root (and
.dark for dark mode); every utility that references it updates at once. To tweak a few tokens,
import a small override after hope/theme.css so it wins:
/* imported AFTER @hope-ui/presets/hope/theme.css, so these win */
:root {
--hope-primary: var(--color-emerald-600);
--hope-primary-hovered: var(--color-emerald-700);
--hope-on-primary: var(--color-white);
}
.dark {
--hope-primary: var(--color-emerald-400);
}To restyle the whole palette, skip hope/theme.css entirely and import your own theme.css in its
place — a complete :root/.dark token set, exactly what the Theme Creator copies
out. Either way, token values live in CSS, not on the preset object — which is why hope can be a
zero-DOM preset and still carry a full palette.
Dark mode
hope ships light and dark values for every token. Toggle the .dark class on a root element
(typically <html>) and the whole token set swaps — no component or utility changes needed.
Recipes (going further)
tailwind-variants recipe — a pure prop→className function. You rarely author recipes directly; slotClasses,class, and definePreset cover almost everything. Presets keep recipes pure (they reference finished tokens only, never a computed color), which is what makes swapping a preset safe.