hope-uiearly preview

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):

UtilityWhat 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}-softThe tonal, low-emphasis fill.
text-{role}-emphasisRole-colored text on a neutral surface.
border-{role}-lineThe role’s border color.

Alongside the roles, a neutral vocabulary covers surfaces, text, borders, and system colors:

UtilityWhat it paints
bg-surface / -raised / -sunken / -inverseElevation — the page, a raised card, a sunken well, an inverse surface.
text-foreground / -muted / -subtleThe neutral text ramp, strongest to faintest.
border-subtle / -strongNeutral borders and dividers.
ring-focusThe 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.

theme.ts
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

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:

brand.css
/* 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.