Nightwind: Automatic Dark Mode for Tailwind Without Writing dark: Everywhere
Hook
What if you could add dark mode to your entire Tailwind application by changing exactly one line of config and adding zero dark: prefixes to your markup? That's the promise—but the reality has nuance.
Context
Every developer who's added dark mode to a Tailwind project knows the ritual: sprinkle dark: variants across every component, remember which colors need inversion, test constantly to ensure nothing looks broken. A bg-white becomes dark:bg-gray-900, text-gray-900 becomes dark:text-gray-100, and suddenly you're maintaining two complete color schemes in parallel.
This manual approach gives perfect control but scales poorly. On a marketing site with hundreds of components, you'll add thousands of dark: classes. Miss one bg-white deep in a nested component and you'll ship a blinding white flash to users who prefer dark mode. Nightwind emerged as a pragmatic middle ground: what if the computer could invert your colors automatically, following Tailwind's numeric scale, and you only intervened when the automatic choice looked wrong? The plugin leverages Tailwind's existing dark mode infrastructure but flips the workflow—automatic by default, manual overrides where needed.
Technical Insight
Nightwind operates at the Tailwind build phase, injecting itself as a plugin that generates inverted color variants. When you write bg-red-600, Nightwind generates a corresponding .dark .bg-red-600 rule that applies bg-red-300 instead. The inversion follows a simple principle: lighter shades (100-400) become darker (900-600), and vice versa. The default mapping is 50↔900, 100↔800, 200↔700, 300↔600, 400↔500, creating visual balance across the color scale.
The installation is deliberately minimal. After npm install nightwind, you add it to your Tailwind config:
// tailwind.config.js
module.exports = {
darkMode: 'class',
plugins: [require('nightwind')],
}
Then initialize dark mode handling in your app. For a React or Next.js project, you'd add the initialization script to prevent flash of unstyled content:
import nightwind from 'nightwind/helper'
export default function App({ Component, pageProps }) {
return (
<>
<script dangerouslySetInnerHTML={{ __html: nightwind.init() }} />
<Component {...pageProps} />
</>
)
}
The nightwind.init() function generates an inline script that runs before React hydrates. It checks localStorage for saved preferences, falls back to system prefers-color-scheme, and immediately applies the dark class to the document root. This pre-render execution eliminates the jarring light-to-dark flash that plagues most client-side theme implementations.
Toggling themes becomes a one-liner. The helper exposes a toggle function that manages the class, localStorage persistence, and optional transition effects:
import nightwind from 'nightwind/helper'
function ThemeToggle() {
return (
<button onClick={() => nightwind.toggle()}>
Toggle Dark Mode
</button>
)
}
Under the hood, the plugin hooks into Tailwind's addVariant API to generate the inverted rules. It scans your config for color values, creates the inverse mappings, and outputs additional CSS. If you use bg-blue-500 somewhere in your markup, Nightwind ensures .dark .bg-blue-500 exists with bg-blue-400 (or whatever your color map specifies) applied.
Customization happens through the nightwind config object. You can remap specific colors when the automatic inversion produces poor results:
// tailwind.config.js
module.exports = {
darkMode: 'class',
plugins: [require('nightwind')],
nightwind: {
colors: {
white: 'gray.900',
black: 'gray.50',
red: {
100: 'red.900',
500: 'red.600',
},
},
},
}
This tells Nightwind that when it encounters bg-white in dark mode, use bg-gray-900 instead of the default inversion. It's surgical—you override only what needs fixing while leaving the rest automatic.
For elements that shouldn't invert at all, you add the nightwind-prevent class. This is crucial for logos, images, or brand colors that must remain consistent across themes:
<div className="bg-brand-500 nightwind-prevent">
Brand color stays exactly the same in dark mode
</div>
The plugin also handles transitions through the nightwind-transition helper class, which applies smooth color changes when toggling. You can customize the transition duration and easing in the config, though the default 300ms ease works for most cases.
One clever architectural choice: Nightwind doesn't use CSS variables or runtime JavaScript for the color swapping. Everything happens at build time in the generated CSS. This means zero runtime overhead—dark mode is just CSS class toggling, as performant as Tailwind's native dark: variant. The tradeoff is larger CSS bundles since you're effectively generating two complete color systems.
Gotcha
The automatic inversion works beautifully for grays and neutral UI elements but breaks down with brand colors and saturated hues. Your brand's primary blue might be carefully chosen for accessibility on white backgrounds. Invert it automatically and you might get a shade that fails contrast checks or looks washed out against dark gray. You'll spend time identifying these cases and adding overrides, which partially defeats the zero-config promise.
The CSS bundle size grows significantly. If your project uses 50 different color utilities across various shades, Nightwind generates 50 additional dark mode variants. For large applications with aggressive color usage, this can add 30-50KB to your stylesheet. PurgeCSS helps, but you're still shipping roughly double the color-related CSS compared to a selective dark: approach where you only add variants to the 20% of elements that actually need them. Additionally, the plugin only works with Tailwind's class-based dark mode strategy—if your project uses media query-based dark mode or a different CSS framework's dark mode system, Nightwind won't help. The tight coupling to Tailwind's architecture is both its strength and limitation.
Verdict
Use if: You're building content-heavy sites (blogs, documentation, marketing pages) where most colors are grays and neutrals that invert predictably, prototyping quickly and need dark mode without slowing down, or inheriting a large Tailwind codebase that lacks dark mode and manually adding thousands of dark: classes sounds nightmarish. The automatic inversion will handle 70-80% correctly, and you'll override the rest. Skip if: You're working on a design-system-driven product where brand colors and precise dark mode aesthetics are critical to the user experience, your CSS bundle size is already problematic (Nightwind adds overhead), or you're on a small project where manually adding dark: to 30 components is faster than learning another plugin. Nightwind trades precision for velocity—only you know which your project needs more.