@dheme/react
v2.15.0
Published
React bindings for Dheme SDK with zero-FOUC theme application
Downloads
1,606
Maintainers
Readme
@dheme/react
React bindings for the Dheme Theme Generator API. Apply production-ready themes to your React app with a single provider — zero FOUC on any visit after the first.
Built for React SPAs (Vite, CRA, Remix SPA mode). For Next.js App Router, use @dheme/next instead.
Installation
npm install @dheme/react @dheme/sdkyarn add @dheme/react @dheme/sdkpnpm add @dheme/react @dheme/sdkRequirements
| Dependency | Version |
| ------------ | -------- |
| react | >= 18 |
| react-dom | >= 18 |
| @dheme/sdk | >= 1.1.0 |
@dheme/sdk is included as a dependency and will be installed automatically. You only need to install it explicitly if you want to use the SDK client directly.
Quick Start
import { DhemeProvider, DhemeScript } from '@dheme/react';
function App() {
return (
<>
<DhemeScript />
<DhemeProvider apiKey="dheme_abc12345_..." theme="#3b82f6">
<YourApp />
</DhemeProvider>
</>
);
}That's it. Your app now has 19 CSS variables applied to :root — fully compatible with shadcn/ui and Tailwind CSS.
How It Works
First visit (no cache)
- React mounts,
DhemeProvidercalls the Dheme API - Theme is applied as CSS variables on
:root - Theme is cached in
localStoragefor next visit
Subsequent visits (cached — zero FOUC)
The provider applies the cached theme synchronously during React's first render — before the browser paints. No loading overlay is shown:
- React mounts,
DhemeProviderreadslocalStorageand applies CSS variables immediately in itsuseStateinitializer isReadystarts astrue, children render with the correct theme from the first paint- A background API request revalidates the cache without blocking anything
Adding <DhemeScript> for first-visit FOUC prevention
For apps that want zero FOUC even on the very first visit (before the cache is populated), add <DhemeScript> to your document <head>. It injects a tiny ~800-byte blocking script that runs before React loads:
// index.html / _document.tsx
<head>
<DhemeScript defaultMode="dark" />
</head>Without it, the first visit shows a loading overlay while the API call completes. With it, no overlay is shown even on the first visit (if there is a cache from a previous visit in the same browser).
Components
<DhemeProvider>
The main provider. Manages theme state, API calls, caching, and CSS variable application.
<DhemeProvider
apiKey="dheme_abc12345_..." // Required — your Dheme API key
theme="#3b82f6" // Primary color (auto-generates on mount)
themeParams={{
// Optional generation params
radius: 0.75,
saturationAdjust: 10,
secondaryColor: '#10b981',
borderIsColored: false,
tailwindVersion: 'v4', // 'v3' | 'v4' (default: 'v4')
}}
defaultMode="light" // 'light' | 'dark' (default: 'light')
persist={true} // Cache in localStorage (default: true)
autoApply={true} // Apply CSS vars automatically (default: true)
onThemeChange={(theme) => {}} // Callback when theme changes
onModeChange={(mode) => {}} // Callback when mode changes
onError={(error) => {}} // Callback on error
>
<App />
</DhemeProvider>| Prop | Type | Default | Description |
| --------------- | ---------------------------------------- | --------- | ---------------------------------------------------- |
| apiKey | string | - | Required. Your Dheme API key. |
| theme | string | - | Primary HEX color. Auto-generates on mount. |
| themeParams | Omit<GenerateThemeRequest, 'theme'> | - | Additional generation parameters. |
| defaultMode | 'light' \| 'dark' | 'light' | Initial color mode. |
| baseUrl | string | - | Override API base URL. |
| persist | boolean | true | Cache theme in localStorage. |
| autoApply | boolean | true | Apply CSS variables to :root. |
| onThemeChange | (theme: GenerateThemeResponse) => void | - | Called when theme data changes. |
| onModeChange | (mode: ThemeMode) => void | - | Called when mode changes. |
| onError | (error: Error) => void | - | Called on API errors. |
| loadingContent | React.ReactNode | - | Content rendered inside the loading wrapper on first API call. |
themeParams.tailwindVersioncontrols the CSS variable format applied to:root. Use'v3'for projects that wrap variables withhsl(var(--token))(Tailwind v3 / shadcn/ui default), or'v4'(default) for projects that usevar(--token)directly (Tailwind v4 /@theme inline).
<DhemeScript>
Optional blocking script for zero FOUC on the very first visit. On cached visits, the provider already handles FOUC prevention internally — DhemeScript adds protection for first-visit scenarios.
<DhemeScript
defaultMode="light" // Must match the defaultMode in DhemeProvider
nonce="abc123" // CSP nonce (optional)
/>Place it in your HTML <head> before React loads. The defaultMode must match the value used in <DhemeProvider>.
| Prop | Type | Default | Description |
| ------------- | ------------------- | --------- | ---------------------------------------------------------------- |
| defaultMode | 'light' \| 'dark' | 'light' | Fallback mode when no preference is stored. Match DhemeProvider. |
| nonce | string | - | CSP nonce for the script. |
Hooks
useTheme()
Read theme data. Only re-renders when theme data or mode changes — not when loading state changes.
import { useTheme } from '@dheme/react';
function MyComponent() {
const { theme, mode, isReady } = useTheme();
if (!isReady) return <Skeleton />;
return <p>Primary: {theme.colors[mode].primary.h}°</p>;
}| Return | Type | Description |
| --------- | ------------------------------- | ---------------------------- |
| theme | GenerateThemeResponse \| null | The full theme data. |
| mode | 'light' \| 'dark' | Current color mode. |
| isReady | boolean | true once theme is loaded. |
useThemeActions()
Access actions and loading state. Components using this hook re-render on action state changes — components using only useTheme() do not.
import { useThemeActions } from '@dheme/react';
function ThemeToggle() {
const { setMode, isLoading } = useThemeActions();
return (
<button disabled={isLoading} onClick={() => setMode(mode === 'light' ? 'dark' : 'light')}>
Toggle
</button>
);
}| Return | Type | Description |
| --------------- | ------------------------------------------------- | ------------------------ |
| generateTheme | (params: GenerateThemeRequest) => Promise<void> | Generate a new theme. |
| setMode | (mode: ThemeMode) => void | Switch light/dark mode. |
| clearTheme | () => void | Clear theme and cache. |
| isLoading | boolean | true during API call. |
| error | Error \| null | Last error, if any. |
| client | DhemeClient | Raw SDK client instance. |
useGenerateTheme()
Convenience hook with local loading state — useful when multiple components trigger generation independently.
import { useGenerateTheme } from '@dheme/react';
function ColorPicker() {
const { generateTheme, isGenerating, error } = useGenerateTheme();
return (
<button disabled={isGenerating} onClick={() => generateTheme({ theme: '#ef4444' })}>
{isGenerating ? 'Generating...' : 'Apply Red'}
</button>
);
}useDhemeClient()
Direct access to the DhemeClient instance for advanced operations (e.g., getUsage()).
import { useDhemeClient } from '@dheme/react';
function UsageInfo() {
const client = useDhemeClient();
const [usage, setUsage] = useState(null);
useEffect(() => {
client.getUsage().then(({ data }) => setUsage(data));
}, [client]);
return usage ? <p>{usage.remaining} requests left</p> : null;
}Context Splitting (Performance)
The provider uses two separate React contexts to minimize re-renders:
| Context | Contains | Changes when |
| --------------------- | ------------------------------------------------ | -------------------------- |
| ThemeDataContext | theme, mode, isReady | Theme data or mode changes |
| ThemeActionsContext | generateTheme, setMode, isLoading, error | Actions are triggered |
Components using useTheme() (data) do not re-render when isLoading changes.
Components using useThemeActions() (actions) do not re-render when theme data changes.
This prevents cascading re-renders in large component trees.
Caching
Themes are cached in localStorage with deterministic keys based on input parameters:
same input params → same cache key → same themeThe cache key is derived from: theme, secondaryColor, radius, saturationAdjust, lightnessAdjust, contrastAdjust, cardIsColored, backgroundIsColored, borderIsColored.
Stale-while-revalidate
On cached visits, the provider:
- Serves the cached theme immediately (zero latency)
- Fires a background API request to check for updates
- Only updates the UI if the response differs from the cache
This ensures instant page loads while keeping themes fresh.
Mode Switching
Switching between light and dark mode does not make an API call. Both colors.light and colors.dark are included in a single API response, so mode switching is instant.
const { setMode } = useThemeActions();
// Instant — no network request
setMode('dark');The provider also syncs the dark class on <html> automatically.
CSS Variables
The provider sets 19 CSS variables + --radius on :root. The value format depends on themeParams.tailwindVersion:
Tailwind v4 (default) — wrapped hsl(), use with var(--token):
:root {
--background: hsl(0 0% 100%);
--foreground: hsl(222.2 84% 4.9%);
--primary: hsl(221.2 83.2% 53.3%);
--primary-foreground: hsl(210 40% 98%);
/* ... 15 more tokens */
--radius: 0.5rem;
}Tailwind v3 (tailwindVersion: 'v3') — bare channels, use with hsl(var(--token)):
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
/* ... 15 more tokens */
--radius: 0.5rem;
}ThemeGenerator
A floating FAB (Floating Action Button) that lets users generate and preview themes in real time — directly inside your app. No external dependencies beyond React itself.
import { DhemeProvider, ThemeGenerator } from '@dheme/react';
function App() {
return (
<DhemeProvider apiKey="..." theme="#3b82f6">
<MyApp />
<ThemeGenerator />
</DhemeProvider>
);
}The component renders as a pill in the corner of the screen. Clicking it expands a panel with color pickers, sliders, and toggles that call generateTheme() in real time with per-parameter debounce.
Props
<ThemeGenerator
defaultTheme="#4332f6" // Initial primary color
defaultSecondaryColor="#ab67f1" // Initial secondary color
defaultSecondaryEnabled={false} // Whether secondary starts enabled
defaultSaturation={10} // Initial saturation adjust (-100 to 100)
defaultLightness={2} // Initial lightness adjust (-100 to 100)
defaultRadius={0} // Initial border radius (0 to 2 rem)
defaultBackgroundIsColored={false} // Initial colorful background toggle
position="bottom-right" // 'bottom-right' | 'bottom-left'
open={isOpen} // Controlled open state (optional)
onOpenChange={setIsOpen} // Controlled open callback (optional)
labels={{
// i18n overrides (all optional)
title: 'Theme Generator',
primary: 'Primary Color',
secondary: 'Secondary Color',
saturation: 'Vibrancy',
lightness: 'Brightness',
reset: 'Restore defaults',
}}
className="my-fab" // Extra class on the container (optional)
/>| Prop | Type | Default | Description |
| ---------------------------- | --------------------------------- | ---------------- | ------------------------------------------------ |
| defaultTheme | string | '#4332f6' | Initial primary HEX color. |
| defaultSecondaryColor | string | '#ab67f1' | Initial secondary HEX color. |
| defaultSecondaryEnabled | boolean | false | Whether secondary color starts enabled. |
| defaultSaturation | number | 10 | Initial saturation adjust (-100–100). |
| defaultLightness | number | 2 | Initial lightness adjust (-100–100). |
| defaultRadius | number | 0 | Initial border radius (0–2 rem). |
| defaultBackgroundIsColored | boolean | false | Initial state of the colorful background toggle. |
| position | 'bottom-right' \| 'bottom-left' | 'bottom-right' | Corner to anchor the FAB. |
| open | boolean | — | Controlled open state. Omit for uncontrolled. |
| onOpenChange | (open: boolean) => void | — | Called when open state changes. |
| labels | ThemeGeneratorLabels | See below | Override any UI text for i18n. |
| className | string | — | Extra CSS class on the fixed container. |
Default labels
| Key | Default |
| -------------------- | ----------------------------------------------------------------------------- |
| title | 'Theme Generator' |
| description | 'Generate complete themes from a single color. Changes apply in real time.' |
| baseColors | 'Base Colors' |
| primary | 'Primary' |
| secondary | 'Secondary' |
| optional | 'Optional' |
| fineTuning | 'Fine Tuning' |
| saturation | 'Saturation' |
| lightness | 'Lightness' |
| borderRadius | 'Border Radius' |
| advancedOptions | 'Advanced Options' |
| colorfulCard | 'Colorful Card' |
| colorfulBackground | 'Colorful Background' |
| colorfulBorder | 'Colorful Border' |
| reset | 'Reset' |
| fabPrimaryLabel | 'Primary' |
How it works
Real-time generation with debounce
Each parameter has its own debounce timer. Dragging the saturation slider fires an API call 200ms after the user stops — not on every frame. Color pickers debounce at 150ms.
| Control | Debounce | | ------------------------------------------ | ------------------------ | | Color pickers | 150ms | | Sliders (saturation, lightness, radius) | 200ms | | Boolean toggles (card, background, border) | None — fires immediately | | Secondary color enable/disable | None — fires immediately |
No re-render loops
State local to the component (localPrimary, localSaturation, …) is the source of truth for the controls. The component does not read back from useTheme(). This prevents a loop where generateTheme() → theme state update → re-render → re-initialize state → generateTheme() again.
Zero external dependencies
- Icons: inline SVG (no
lucide-react) - Color picker: built-in gradient + hue slider using pointer events (no
react-colorful) - Styling: inline styles using
hsl(var(--...))CSS variables — automatically matches the active theme
Controlled vs uncontrolled
Omit open / onOpenChange for fully uncontrolled behavior. Provide both for external control:
const [open, setOpen] = useState(false);
<ThemeGenerator open={open} onOpenChange={setOpen} />
<button onClick={() => setOpen(true)}>Open Theme Editor</button>Placement
ThemeGenerator must be a descendant of <DhemeProvider> — it consumes useThemeActions() internally.
// ✅ Correct
<DhemeProvider ...>
<App />
<ThemeGenerator />
</DhemeProvider>
// ❌ Wrong — outside the provider
<>
<ThemeGenerator />
<DhemeProvider ...>
<App />
</DhemeProvider>
</>Utilities
themeToCSS(theme, mode, tailwindVersion?)
Convert a GenerateThemeResponse to a CSS variable assignment string.
import { themeToCSS } from '@dheme/react';
// Tailwind v4 (default) — hsl() wrapped
const css = themeToCSS(theme, 'light');
// "--background:hsl(0 0% 100%);--foreground:hsl(222.2 84% 4.9%);..."
// Tailwind v3 — bare channels
const css = themeToCSS(theme, 'light', 'v3');
// "--background:0 0% 100%;--foreground:222.2 84% 4.9%;..."applyThemeCSSVariables(theme, mode, tailwindVersion?)
Manually apply CSS variables to :root.
import { applyThemeCSSVariables } from '@dheme/react';
applyThemeCSSVariables(theme, 'dark'); // Tailwind v4 (default)
applyThemeCSSVariables(theme, 'dark', 'v3'); // Tailwind v3removeThemeCSSVariables()
Remove all Dheme CSS variables from :root.
buildCacheKey(params)
Generate the deterministic cache key for a set of params.
import { buildCacheKey } from '@dheme/react';
const key = buildCacheKey({ theme: '#3b82f6', radius: 0.75 });Full Example (Vite)
// main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { DhemeProvider, DhemeScript } from '@dheme/react';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<DhemeScript />
<DhemeProvider
apiKey={import.meta.env.VITE_DHEME_API_KEY}
theme="#3b82f6"
themeParams={{ radius: 0.5 }}
>
<App />
</DhemeProvider>
</React.StrictMode>
);// App.tsx
import { useTheme, useThemeActions } from '@dheme/react';
export default function App() {
const { theme, mode, isReady } = useTheme();
const { setMode } = useThemeActions();
if (!isReady) return <div>Loading theme...</div>;
return (
<div>
<h1
style={{
color: `hsl(${theme.colors[mode].primary.h} ${theme.colors[mode].primary.s}% ${theme.colors[mode].primary.l}%)`,
}}
>
Dheme Theme
</h1>
<button onClick={() => setMode(mode === 'light' ? 'dark' : 'light')}>
{mode === 'light' ? 'Dark' : 'Light'} Mode
</button>
</div>
);
}TypeScript
All types are exported:
import type {
ThemeMode,
ThemeDataState,
ThemeActionsState,
DhemeProviderProps,
DhemeScriptProps,
GenerateThemeRequest,
GenerateThemeResponse,
ColorTokens,
HSLColor,
} from '@dheme/react';Related Packages
| Package | Description | When to use |
| -------------- | ----------------------------- | -------------------------- |
| @dheme/sdk | Core TypeScript SDK | Direct API access, Node.js |
| @dheme/react | React bindings (this package) | Vite, CRA, React SPAs |
| @dheme/next | Next.js App Router bindings | Next.js 14+ with SSR |
License
MIT
