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
bg-primary text-on-primary hover:bg-primary-hovered — repoint the token and every use follows.
Customizing
Component defaults and 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
(normally <html>) and the whole token set swaps — no component or utility changes needed.
Recipes
Under each component’s styling is a tailwind-variants recipe: a pure prop-to-className
function. You are unlikely to author one directly, because slotClasses, class, and
definePreset cover the cases above without it.
What matters about recipes from the outside is that presets keep them pure — a recipe references finished tokens only, never a computed color. That is what makes swapping a preset safe: no recipe can bake in a color the next preset doesn’t have.