krishna@
~/projects
liveopen source · 2025 →

@teispace/next-themes

Zero-flash theme manager for Next.js.

@teispace/next-themes — preview
01 // overview

Drop-in replacement for the unmaintained next-themes. Zero-flash SSR, hybrid storage, view transitions, fully typed themes — none of which the original supports.

02 // the.story

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 elsewhere

You 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.