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

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 (typically <html>) and the whole token set swaps — no component or utility changes needed.

Recipes (going further)