@k8ordo/color-scheme

API

The component, hook, values and types @k8ordo/color-scheme exports. There is one entry point, @k8ordo/color-scheme.

On this page

ColorSchemeProvider

Import from @k8ordo/color-scheme

The provider that goes in the root layout, inside <body>, around the whole page. It renders the inline script that puts dark on <html> before the first paint, and keeps the class in step afterwards.

ts
ColorSchemeProvider(props: ColorSchemeProviderProps): ReactNode

Parameters

defaultPreferenceColorSchemePreference = 'system'
What a visitor who chose nothing gets. 'system' follows the OS setting; 'light' or 'dark' holds every visitor there until they choose. Defaults to 'system'.
noncestring | undefined
The nonce to put on the inline script; under @k8ordo/server, pass nonce(). Leave it out when the script is allowed by hash.
childrenReactNode
The whole page. The script is rendered ahead of it.

Caveats

  • It is exported from a 'use client' module, so a root layout that is a Server Component renders it as it is.
  • Give <html> suppressHydrationWarning. The class the script adds is not in the server’s HTML, and React reports it as a mismatch in development.
  • Put exactly one in the application. Each provider renders its own script and writes the class, so two of them can disagree.
  • A server can read neither the stored choice nor the OS setting, so it renders the default; with a 'system' default, that is 'light'.
layout.tsx
export default function RootLayout({
  children,
}: {
  children: ReactNode;
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ColorSchemeProvider>{children}</ColorSchemeProvider>
      </body>
    </html>
  );
}

useColorScheme

Import from @k8ordo/color-scheme

Returns the scheme on screen, the visitor’s choice, and a function that changes it. Use it in Client Components.

ts
useColorScheme(): UseColorScheme

Returns

UseColorScheme — An object holding scheme, preference and setPreference.

Fields

scheme'light' | 'dark'
What is on screen: from the visitor’s choice, the provider’s default, or the OS setting.
preference'light' | 'dark' | 'system'
What the visitor chose; 'system' while nothing is chosen.
setPreference(preference: ColorSchemePreference) => void
Stores a choice. 'system' clears the stored one, so the provider’s default applies again.

Caveats

  • Called outside the provider, it throws an error starting with useColorScheme needs <ColorSchemeProvider> above it. It never quietly falls back to the default.
  • It only reads what the provider decided, and never touches the class on <html> or localStorage.
  • In the server render and the hydration render it returns the server’s guess. The stored choice and the OS setting are read from the render right after.
scheme-toggle.tsx
const { scheme, setPreference } = useColorScheme();

<button
  onClick={() => {
    setPreference(scheme === 'dark' ? 'light' : 'dark');
  }}
  type="button"
>
  Toggle
</button>

See “Build a switch” for a toggle and a three-way choice.

colorSchemeState

Import from @k8ordo/color-scheme

Where the preference lives: the @k8ordo/state defineLocalState definition itself. It is stored in localStorage under k8ordo-state:color-scheme, as a row with one field, preference.

ts
const colorSchemeState = defineLocalState(
  'color-scheme',
  z.object({ preference: z.optional(z.enum(['light', 'dark'])) }),
);

Fields

kind'local'
The kind of place the state lives in: 'local', for localStorage.
key'color-scheme'
Its name within @k8ordo/state. There is one store per name.
schemaZodMiniObject
The schema the definition was given.
storageKey'k8ordo-state:color-scheme'
The localStorage key: k8ordo-state: followed by key.
inlineRead() => string
Returns a JavaScript expression for an inline script. It evaluates to the stored row’s object, or to null when the row cannot be read.

Caveats

  • useAppState(colorSchemeState) reads the stored preference without the hook. It returns 'light' | 'dark' | undefined, not the resolved scheme.
  • Change the choice through setPreference. A localStorage.setItem in the same tab goes unnoticed by the provider until a reload.
  • Do not define another defineLocalState('color-scheme', …) in the application. Stores are shared by name, so the two definitions would fight over one row.
stored-preference.tsx
const [{ preference }] = useAppState(colorSchemeState);
// 'light' | 'dark' | undefined

colorSchemeScriptHash

Import from @k8ordo/color-scheme

Resolves to the SHA-256 of the provider’s inline script as a CSP source, 'sha256-…', for a policy that allows the script without a nonce.

ts
colorSchemeScriptHash(
  defaultPreference?: ColorSchemePreference,
): Promise<string>

Parameters

defaultPreferenceColorSchemePreference = 'system'
The defaultPreference the provider is given. The default is written into the script, so the hash depends on it.

Returns

Promise<string> — A CSP source such as 'sha256-…', quotes included.

Caveats

  • Call it wherever the policy is written, every time, rather than copying its value: the script is the installed version’s, and an update may change it.
  • Where every response can carry a nonce, as under @k8ordo/server, the hash is not needed.
vite.config.ts
framework({
  csp: {
    'script-src': ["'self'", await colorSchemeScriptHash()],
  },
});

ColorScheme

Import from @k8ordo/color-scheme

A scheme that can be on screen; the type of scheme.

ts
type ColorScheme = 'light' | 'dark';

ColorSchemePreference

Import from @k8ordo/color-scheme

A scheme, or 'system'. As a visitor’s choice, 'system' is nothing chosen, so the provider’s default applies; as that default, it follows the OS setting.

ts
type ColorSchemePreference = ColorScheme | 'system';

ColorSchemeProviderProps

Import from @k8ordo/color-scheme

The props of ColorSchemeProvider, for a component of your own that wraps it.

ts
type ColorSchemeProviderProps = {
  readonly defaultPreference?: ColorSchemePreference;
  readonly nonce?: string;
  readonly children: ReactNode;
};

UseColorScheme

Import from @k8ordo/color-scheme

What useColorScheme() returns, for a component that receives it as props.

ts
type UseColorScheme = {
  readonly scheme: ColorScheme;
  readonly preference: ColorSchemePreference;
  readonly setPreference: (preference: ColorSchemePreference) => void;
};
k8ordo

React libraries that use Baseline features without holding back.

© 2026 k8o — MIT License

Typeset in Noto Sans JP & M PLUS 2