@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. Paco Coursey'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 a standard browser API, and the package doesn't catch up. Last release: 0.4.6, March 2025. Last commit: May 2025. Open issues: 45. Stars: 6.3K. 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.6",
+ "@teispace/next-themes": "^2.0.4"
}
}// 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 small library that thousands of apps depend on is unpaid emotional labor. Paco 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 a real release cadence and a real test pipeline. It now serves around 11,000 downloads a week, which is the only review that matters: people put it in production and left it there. If next-themes is in your stack, swap it in and never think about theme flash again.