@teispace/next-themes
Zero-flash theme manager for Next.js.
Drop-in replacement for the unmaintained next-themes. Zero-flash SSR, hybrid storage, view transitions, fully typed themes — none of which the original supports.
the maintainer problem
The original next-themes is one of those small libraries that became infrastructure. Spencer's package taught a generation of Next.js apps how to do dark mode without flashing a wrong-color paint frame on first load. It was good. And then it stopped getting updates.
The story is familiar: a successful open-source maintainer, used by tens of thousands of apps, stops responding to issues. Bugs accumulate. Pull requests sit. The Next.js team ships React 19, view transitions become standard browser API, and the package doesn't catch up. Last commit: 2024-04. Issues open: many. Stars: 5K+. People still depend on it.
If you are one of those tens of thousands of apps, you have three options: live with the bugs, fork it for yourself, or wait. I picked option two and made the fork installable for everyone.
what @teispace/next-themes is
It's a drop-in fork. Same public API surface — useTheme(), <ThemeProvider />, themes prop, attribute="class" — so migrating an existing app is a one-line import change in your package.json:
{
"dependencies": {
- "next-themes": "^0.4.0",
+ "@teispace/next-themes": "^0.2.0"
}
}// Before
import { ThemeProvider, useTheme } from 'next-themes';
// After
import { ThemeProvider, useTheme } from '@teispace/next-themes';That's the migration. Behind the same API, the internals are rebuilt to fix the things the upstream couldn't get to.
what's actually different inside
Zero-flash, properly
The original injects a synchronous <script> into <head> that reads localStorage and sets the theme class on <html> before paint. This is correct, but the script string was generated at build time and shipped as a literal — small enough, but rigid. You couldn't easily extend it for things like cookie-based reads or system-pref listening at boot.
The fork rewrites this as a typed function that's serialized at render time. You can pass theme tokens, fallback strategy, and storage policy — and the head-script reflects them. No flash, configurable.
Hybrid storage
If your app is server-rendered, the server can't read localStorage. Without help, you ship the wrong theme on first paint and correct it after hydration — the original problem the library was supposed to solve.
The fork supports a hybrid model: theme is mirrored to a cookie (SSR-readable) and localStorage (offline-readable). The provider takes a storage prop:
<ThemeProvider storage="hybrid" attribute="class">On the server, you read the cookie in your root layout and pass the resolved theme as a prop. On the client, the head-script reconciles both stores. The flash window goes from "until hydration" to "never."
View transitions, native
Browsers ship document.startViewTransition() now. Toggling theme should be a smooth crossfade rather than a jarring flip. The fork wraps the theme write in a transition automatically when supported:
const { setTheme } = useTheme();
setTheme('dark'); // crossfades on supported browsers, instant elsewhereYou can opt out (disableViewTransition) or supply your own callback for custom animations.
Typed themes
The original is stringly-typed: themes={['light', 'dark', 'sepia']} means setTheme(string) — typo at your peril.
The fork accepts a typed registry:
const THEMES = ['light', 'dark', 'sepia'] as const;
type Theme = (typeof THEMES)[number];
<ThemeProvider<Theme> themes={THEMES} attribute="class">useTheme<Theme>() then returns Theme | undefined for resolvedTheme — autocompletion in your editor, errors at build time, no as Theme casts.
React 19 + Next.js 16 ready
The original predates the React Compiler, the new use-cache primitives, and several Next.js 16 lifecycle changes. The fork tracks each.
the 30-line head-script
The single most important file in the package is the script that runs before React hydrates. It has to be small (every byte is paint-blocking), correct (a single bug here means every app flashes), and stable across browsers (no modern syntax that an old Safari can't parse).
A heavily-annotated version:
;(() => {
// Read in priority order: cookie (SSR-aware) → localStorage → system pref.
const c = document.cookie.match(/(?:^|;\s*)theme=([^;]+)/)?.[1];
let t = c || localStorage.getItem('theme');
if (!t || t === 'system') {
t = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
// Apply via the configured attribute (class or data-theme).
const e = document.documentElement;
if (e.classList.contains(t)) return;
e.classList.add(t);
})()That's the entire flash-prevention contract. It runs synchronously in <head>, so paint never happens with the wrong class.
the test that matters
Hydration mismatches are silent killers. If the server renders one theme class and the client renders a different one, React 19 will throw a (possibly suppressed) hydration warning and the layout shifts. The fork has a single Playwright test that mounts every supported framework setup (App Router, Pages Router, server-prefetched, client-only) under both themes and screenshots both the server-side first paint and the client-hydrated paint, then compares. Passing means no flash, ever.
It's the kind of test that takes 20 minutes to write and saves a year of "why does it flash on Safari sometimes."
what's next
- Theme schedules — light during the day, dark after sunset, all defined at the provider level.
- Per-component theme overrides via React Context (some apps want the homepage in dark even when the user is on light).
- A migration guide for next-themes v0.3 → v0.4 → @teispace/next-themes (the upstream had a breaking 0.3 → 0.4 that some apps still haven't done).
the meta-point
Maintaining a 200-line library used by 10,000 apps is unpaid emotional labor. Spencer did it for years. When that stops, the apps don't stop — they just rot quietly until somebody forks. @teispace/next-themes is a fork that's actively maintained, with weekly releases and a real test pipeline. If next-themes is in your stack, swap it in and never think about theme flash again.