Skip to content

Theming

Change the brand hue, the neutral tint, the radius or the mode — globally or for a single subtree — without touching a component.

Every prfct component reads its colors, radii and shadows from CSS custom properties. Theming means changing those properties; components never need to be edited. Try it live in the theme builder.

LightScoped

Same components, same classes — the subtree sets the mode.

DarkScoped

Same components, same classes — the subtree sets the mode.

Light and dark

prfct ships both modes. The default setup uses next-themes to follow the operating system, persist a choice, and avoid a flash of the wrong theme:

app/layout.tsx
import { ThemeProvider } from "@/components/theme-provider"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}

Scoped themes

Modes are defined on classes, not only on the root, so any subtree can switch:

<aside className="dark"></aside>   {/* a dark panel on a light page */}
<figure className="light"></figure> {/* a light preview on a dark page */}

This is how prfct's own documentation renders light and dark swatches side by side. It works because components express every mode difference through variables — none of them use dark: utilities.

Brand color

The brand scale is generated from three numbers: hue, chroma and the lightness of the solid step. Changing them regenerates all 12 steps for both modes, with text steps re-solved for contrast.

lib/tokens/palette.ts
export const themeConfig = {
  gray: neutralPresets.graphite,
  brand: huePresets.ultramarine, // or { hue: 150, chroma: 0.15, lightness: 0.55 }
}

Run pnpm tokens to rebuild styles/tokens.css. The theme builder does the same thing in the browser and gives you the CSS to paste.

Monochrome brand

Set brand: "mono" and the brand scale follows the neutral one: checked states, progress and focus become black (or white in dark mode). It's the most restrained expression of prfct, and still accessible.

Neutral tint

The gray scale can carry a subtle hue. Seven presets ship with prfct:

PresetCharacter
graphite (default)Barely cool. Reads as pure gray, feels crisper.
neutralChroma 0. Absolute neutrality.
slateCool blue-gray. Technical, calm.
stoneWarm gray. Editorial.
sandWarmer, papery.
mauveViolet-tinted. Pairs with violet and pink brands.
sageGreen-tinted. Pairs with green and teal brands.

Radius

:root {
  --radius: 0.5rem;
}

Every radius step is a multiple of --radius, so 0 gives a sharp, technical interface and 0.75rem a soft, friendly one. See Shape.

Overriding semantic tokens

For one-off brand requirements, override semantic tokens after tokens.css. Prefer mapping to a scale step over a raw color, so dark mode keeps working:

app/globals.css
:root,
.light {
  --primary: var(--brand-9);          /* colored primary buttons */
  --primary-foreground: var(--brand-contrast);
}

.dark {
  --primary: var(--brand-9);
}
Keep status colors fixed
Status hues are not themeable on purpose. People learn that green means success and red means danger across every product they use; your brand shouldn't be able to change that.