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

  • ThemeScript reads localStorage and the system color preference, then sets the html theme attribute before the page paints.
  • useTheme() reads the saved preference in client components and returns theme, resolvedTheme, and setTheme.
  • The default is the dark class on html. Tailwind’s dark: 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

ExportDescription
ThemeScriptRenders 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

OptionDefaultDescription
storageKeypaintdrop-themeThe localStorage key used to remember the preference.
attributeclassUse the dark class or a data attribute such as data-theme.
defaultThemesystemThe preference to use when no saved preference exists.