Getting started
Theming and dark mode
A small React theme utility for light, dark, and system preferences. It applies the document theme before first paint and gives client components a hook for reading and changing the preference.
How it works
ThemeScriptreadslocalStorageand the system color preference, then sets thehtmltheme attribute before the page paints.useTheme()reads the saved preference in client components and returnstheme,resolvedTheme, andsetTheme.- The default is the
darkclass onhtml. Tailwind’sdark:styles can then follow that class.
Server-rendered apps
Render the script directly in the document head. Add suppressHydrationWarning to html because the initializer changes its class or data attribute before hydration.
import { ThemeScript } from '@paintdrop/ui/theme';
export function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<ThemeScript />
{/* Keep the initializer before stylesheets when possible. */}
</head>
<body>{children}</body>
</html>
);
}The initializer is inline JavaScript. Sites with a strict Content Security Policy need to allow it with a hash or nonce; the current ThemeScript API does not accept a nonce yet.
Theme controls
Use the hook from a client component. The server cannot read localStorage, so the select and its icon start from the default preference and update after hydration.
'use client';
import { useTheme, type Theme } from '@paintdrop/ui/theme';
export function ThemeSelect() {
const { theme, setTheme } = useTheme();
return (
<select
aria-label="Color theme"
value={theme}
onChange={(event) => setTheme(event.target.value as Theme)}
>
<option value="dark">Dark</option>
<option value="light">Light</option>
<option value="system">System</option>
</select>
);
}Client-only apps
Choose between calling initializeTheme() from your entrypoint and pasting a script into your HTML. The entrypoint call applies the theme before React renders. For the earliest possible application, paste the inline script into index.html near the start of <head>, before stylesheets and the app bundle.
Entrypoint import
Call the initializer before mounting React. It reads the saved preference or system setting and sets the theme on html. Because client entrypoints run after HTML parsing, a static page shell can still paint before this call executes.
import { initializeTheme } from '@paintdrop/ui/theme';
import { createRoot } from 'react-dom/client';
import { App } from './App';
initializeTheme();
createRoot(document.getElementById('root')!).render(<App />);Manual HTML script
Copy this script into your app's HTML <head>. It is generated from getThemeScript() with the default options, so it works without build configuration.
<head>
<script>(()=>{let t="system";try{const s=localStorage.getItem("paintdrop-theme");if(s==='light'||s==='dark'||s==='system')t=s}catch{}try{const q=matchMedia('(prefers-color-scheme: dark)');const apply=()=>{const r=t==='system'?(q.matches?'dark':'light'):t,e=document.documentElement;if("class"==='class')e.classList.toggle('dark',r==='dark');else e.setAttribute("class",r);e.style.colorScheme=r};apply();q.addEventListener('change',()=>{if(t==='system')apply()})}catch{}})();</script>
<!-- Load stylesheets and the app after the theme initializer. -->
</head>API
| Export | Description |
|---|---|
ThemeScript | Renders the early initializer in a server-rendered document head. |
getThemeScript(options?) | Returns the inline JavaScript for manual HTML placement or build-time injection. |
initializeTheme(options?) | Applies the saved or system theme immediately from a client entrypoint. |
useTheme(options?) | Returns the preference, resolved light/dark value, and setter. |
setTheme(theme, options?) | Persists a preference and applies it to the document immediately. |
Options
| Option | Default | Description |
|---|---|---|
storageKey | paintdrop-theme | The localStorage key used to remember the preference. |
attribute | class | Use the dark class or a data attribute such as data-theme. |
defaultTheme | system | The preference to use when no saved preference exists. |