useTheme hook
interlace-ui/use-themeFreeUtility
@interlace/ui — reads and writes the theme (`data-theme`) and colour scheme (`.dark`) axes, persists to localStorage, and follows the OS when no preference is stored.
Install it
npx shadcn@latest add https://ds.interlace.tools/r/use-theme.jsonSource
1'use client';23import { useCallback, useEffect, useState } from 'react';45// @interlace/use-theme v1.1.0 — Interlace design system.6// Docs, props and live preview: https://ds.interlace.tools/c/use-theme7// What changed since: https://ds.interlace.tools/c/use-theme#history8// Generated banner — keep it, the upgrade diff reads this version.910/**11 * @interlace/ui — useTheme.12 *13 * The runtime half of the two-axis theme contract. It owns the two pieces of14 * state a user can change (which THEME, which SCHEME), persists them, and15 * projects them onto `<html>` in exactly the shape `styles/index.css`16 * expects:17 *18 * theme → `data-theme="<name>"` (absent = the default, which IS `:root`)19 * scheme → `class="dark"` (shadcn / next-themes canon)20 *21 * ─── No dependency, but not no coordination ──────────────────────22 *23 * next-themes is ~3kB to do this, and its API surface (`forcedTheme`,24 * `enableColorScheme`, `nonce`, `themes[]`, a provider, a context) exists to25 * serve apps whose theme list is dynamic. Ours is a compile-time constant26 * with a machine-checked contract behind it, so the provider has nothing to27 * provide: the `<html>` element is the shared state, and a Context would be a28 * SECOND source of truth that can disagree with the DOM after the bootstrap29 * script runs — the exact bug class the script exists to avoid.30 *31 * What the DOM cannot do on its own is tell React that it changed. Two32 * instances of this hook in ONE document — a switcher in the nav and, say, a33 * themed preview frame in the page — each own their own `useState`, so the34 * one that did not handle the click keeps rendering the previous theme35 * forever: `storage` events fire in OTHER documents only, so nothing wakes36 * it. Found exactly that way (Phase 8.4: ds.interlace.tools repainted into37 * Harbor while every embedded preview stayed Interlace-orange), and it is38 * the sort of bug that reads as "theming doesn't work" rather than as a39 * missing subscription.40 *41 * The fix is a module-level subscriber list, not a provider: writes are42 * broadcast to every live instance in the document. It is deliberately the43 * VALUE that is broadcast rather than a "re-read storage" ping, because44 * storage can refuse the write (Safari private mode) and a re-read would45 * then roll the user's click back to the previous theme — turning a46 * degraded-but-working page into a control that visibly does nothing.47 *48// … truncated
More from interlace-ui
All 140 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| cn — class-name merge utilitycn | interlace-ui | Utilities | Free | no deps | |
| No-flash theme bootstrap scripttheme-script | interlace-ui | Utilities | Free | no deps | |
| Theme token manifesttheme-tokens | interlace-ui | Utilities | Free | no deps | |
| useReducedMotion hookuse-reduced-motion | interlace-ui | Utilities | Free | no deps |
