@thiagormoreira/nightwind
v2.8.1
Published
An automatic, overridable, customisable Tailwind dark mode plugin
Maintainers
Readme

IMPORTANT: This repository is a maintained fork of the original "nightwind" project (https://github.com/jjranalli/nightwind). This fork is actively maintained, featuring 100% test coverage, support for modern Tailwind CSS (v3+), and additional features like manual dark: overrides.
A Tailwind CSS plugin that gives you an out-of-the-box, customisable, overridable dark mode.
Nightwind uses the existing Tailwind color palette and your own custom colors to automatically generate the dark mode version of the Tailwind color classes you use.
For example, whenever you use a class like bg-red-600 it gets automatically switched to bg-red-300 in dark mode.
You can see it in action on https://nightwindcss.com
- Installation
- Helper functions
- Animation System
- Getting started
- Configuration
- Color mappings
- Overrides
- Typography
- Tailwind CSS v4 Support
Installation
npm install nightwindEnable the Dark class variant in your tailwind.config.js file.
// tailwind.config.js - Tailwind ^2.0
module.exports = {
darkMode: "class",
// ...
plugins: [require("nightwind")],
}In older Tailwind versions (< 2.0)
// tailwind.config.js
module.exports = {
experimental: {
darkModeVariant: true,
applyComplexClasses: true,
},
dark: "class",
// ...
plugins: [require("nightwind")],
}Helper functions
Nightwind relies on a fixed 'nightwind' class to manage transitions, and a toggled 'dark' class applied on a top level element in the DOM, typically the root element.
You can define your own functions to manage the dark mode (or check the examples below), or use the helper functions included in 'nightwind/helper.js' to get started right away.
By default, the helper functions prevent the dreaded flicker of light mode and allow the chosen color mode to persist on update.
Initialization
To initialize nightwind, add the following script tag to the head element of your pages.
// React Example
import nightwind from "nightwind/helper"
export default function Layout() {
return (
<>
<Head>
<script dangerouslySetInnerHTML={{ __html: nightwind.init() }} />
</Head>
// ...
</>
)
}Toggle
Similarly, you can use the toggle function to switch between dark and light mode.
// React Example — toggle with animation
import nightwind from 'nightwind/helper'
export default function Navbar() {
return (
<>
{/* Simple toggle with default animation */}
<button onClick={() => nightwind.toggle()}>Toggle</button>
{/* Ripple from click point */}
<button onClick={(e) => nightwind.toggle({ animation: 'ripple', event: e })}>
Ripple Toggle
</button>
{/* Slide from left */}
<button onClick={() => nightwind.toggle({ animation: 'slide', direction: 'left' })}>
Slide Toggle
</button>
</>
)
}Enable mode
If you need to selectively choose between light/dark mode, you can use the enable function. It accepts a boolean argument to enable/disable dark mode.
// React Example
import nightwind from "nightwind/helper"
export default function Navbar() {
return (
// ...
<button onClick={() => nightwind.enable(true)}></button>
// ...
)
}BeforeTransition
Nightwind also exports a beforeTransition function that you can leverage in case you prefer to build your own toggle functions. It prevents unwanted transitions as a side-effect of having nightwind [...]
Check out the toggle function in the Nextjs example below for an example of how this could be implemented.
Animation System
Nightwind includes a built-in animation system powered by the View Transitions API. All animations fall back gracefully to a CSS transition for browsers that don't support the API.
Global Configuration
Configure the animation system once via nightwind.configure():
nightwind.configure({
animation: {
default: 'fade', // Default animation for toggle/enable
duration: 600, // Duration in ms
easing: 'cubic-bezier(0.7, 0, 0.3, 1)', // CSS easing or cubic-bezier
reverse: false, // Reverse direction on light mode
slide: {
direction: 'left', // 'left' | 'right' | 'top' | 'bottom'
},
},
transition: {
duration: 400, // CSS transition duration (ms)
easing: 'ease-in-out',
},
persistence: true, // Persist mode in localStorage
storageKey: 'nightwind-mode', // localStorage key
})Per-call Options
Any option can be overridden per call:
// Use ripple just for this click
nightwind.toggle({ animation: 'ripple', event: e })
// Slide from top with custom duration
nightwind.toggle({ animation: 'slide', direction: 'top', duration: 400 })
// Disable animation for this call
nightwind.enable(true, { animation: 'none' })Available Animations
| Animation | Description | Needs event? | Supports direction? | Supports reverse? |
|---|---|---|---|---|
| fade | Cross-fade between themes | No | No | No |
| slide | Page slides in from a direction, moving content | No | Yes (left right top bottom) | Yes |
| reveal | New theme reveals from a direction without moving content | No | Yes (left right top bottom) | Yes |
| ripple | Circular expand/contract from click point | Yes | No | Yes |
| zoom | New theme scales in from center | No | No | No |
| flip | 3D Y-axis rotation between themes | No | No | No |
| rotate | Subtle 2D rotation with scale | No | No | No |
| wipe | Horizontal clip-path wipe | No | No | No |
| iris | Circle opens from center (ripple without click) | No | No | No |
| blur | Blur-out old, blur-in new | No | No | No |
| dissolve | Blur + grayscale cross-fade | No | No | No |
| corner-wipe | Reveal from top-right corner polygon | No | No | No |
| none | Instant switch, no animation | No | No | No |
The reverse Option
When reverse: true, directional animations (slide, reveal) invert their direction when switching to light mode, creating a natural "back and forth" feel:
// Left when going dark, right when going light
nightwind.configure({ animation: { default: 'slide', reverse: true } })For ripple, reverse: true makes the circle contract (instead of expand) when switching to light mode.
CSS Custom Properties
The animation system exposes two CSS custom properties that you can use directly in your own CSS:
| Property | Default | Description |
|---|---|---|
| --nw-anim-duration | 600ms | Duration of the View Transition animation |
| --nw-anim-easing | cubic-bezier(0.7, 0, 0.3, 1) | Easing of the View Transition animation |
| --nightwind-transition-duration | 400ms | Duration of the CSS color transitions |
Browser Support
The View Transitions API is supported in Chrome 111+, Edge 111+, and Safari 18+. Firefox does not yet support it. In unsupported browsers, Nightwind automatically falls back to the CSS transition system (beforeTransition) with no configuration required.
Examples
See examples of implementation (click to expand):
_app.js
Add ThemeProvider using the following configuration
import { ThemeProvider } from "next-themes"
function MyApp({ Component, pageProps }) {
return (
<ThemeProvider
attribute="class"
storageKey="nightwind-mode"
defaultTheme="system" // default "light"
>
<Component {...pageProps} />
</ThemeProvider>
)
}
export default MyAppToggle
Set it up using the useTheme hook
import { useTheme } from "next-themes"
import nightwind from "nightwind/helper"
export default function Toggle(props) {
const { theme, setTheme } = useTheme()
const toggle = () => {
nightwind.beforeTransition()
if (theme !== "dark") {
setTheme("dark")
} else {
setTheme("light")
}
}
return <button onClick={toggle}>Toggle</button>
}index.jsx
Add Helmet using the following configuration
import React from "react"
import ReactDOM from "react-dom"
import { Helmet } from "react-helmet"
import nightwind from "nightwind/helper"
import App from "./App"
import "./index.css"
ReactDOM.render(
<React.StrictMode>
<Helmet>
<script>{nightwind.init()}</script>
</Helmet>
<App />
</React.StrictMode>,
document.getElementById("root")
)Toggle
Set it up using the default example
import nightwind from "nightwind/helper"
export default function Navbar() {
return (
// ...
<button onClick={() => nightwind.toggle()}></button>
// ...
)
}The whole idea is to deconstruct helper.js, converting it from a module to a var. And unpacking the 'init' function from within helper to be its own script body to execut at DOM render. Here is the co[...]
<script>
var nightwind = {
beforeTransition: () => {
const doc = document.documentElement;
const onTransitionDone = () => {
doc.classList.remove('nightwind');
doc.removeEventListener('transitionend', onTransitionDone);
}
doc.addEventListener('transitionend', onTransitionDone);
if (!doc.classList.contains('nightwind')) {
doc.classList.add('nightwind');
}
},
toggle: () => {
nightwind.beforeTransition();
if (!document.documentElement.classList.contains('dark')) {
document.documentElement.classList.add('dark');
window.localStorage.setItem('nightwind-mode', 'dark');
} else {
document.documentElement.classList.remove('dark');
window.localStorage.setItem('nightwind-mode', 'light');
}
},
enable: (dark) => {
const mode = dark ? "dark" : "light";
const opposite = dark ? "light" : "dark";
nightwind.beforeTransition();
if (document.documentElement.classList.contains(opposite)) {
document.documentElement.classList.remove(opposite);
}
document.documentElement.classList.add(mode);
window.localStorage.setItem('nightwind-mode', mode);
},
}
</script>
<script>
(function() {
function getInitialColorMode() {
const persistedColorPreference = window.localStorage.getItem('nightwind-mode');
const hasPersistedPreference = typeof persistedColorPreference === 'string';
if (hasPersistedPreference) {
return persistedColorPreference;
}
const mql = window.matchMedia('(prefers-color-scheme: dark)');
const hasMediaQueryPreference = typeof mql.matches === 'boolean';
if (hasMediaQueryPreference) {
return mql.matches ? 'dark' : 'light';
}
return 'light';
}
getInitialColorMode() == 'light' ? document.documentElement.classList.remove('dark') : document.documentElement.classList.add('dark');
})()
</script>
Getting started
This is some examples of what Nightwind does by default:
- 'bg-white' in dark mode becomes 'bg-black'
- 'bg-red-50' in dark mode becomes 'bg-red-900'
- 'ring-amber-100' in dark mode becomes 'ring-amber-800'
- 'placeholder-gray-200' in dark mode becomes 'placeholder-gray-700'
- 'hover:text-indigo-300' in dark mode becomes 'hover:text-indigo-600'
- 'sm:border-lightBlue-400' in dark mode becomes 'sm:border-lightBlue-500'
- 'xl:hover:bg-purple-500' in dark mode becomes 'xl:hover:bg-purple-400'
Supported classes
Due to file size considerations, Nightwind is enabled by default only on the 'text', 'bg' and 'border' color classes, as well as their 'hover' variants.
You can also extend Nightwind to other classes and variants:
- Color classes: 'placeholder', 'ring', 'ring-offset', 'divide', 'gradient'
- Variants: all Tailwind variants are supported
Configuration
Colors
Nightwind switches between opposite color weights when switching to dark mode. So a -50 color gets switched with a -900 color, -100 with -800 and so forth.
Note: Except for the -50 and -900 weights, the sum of opposite weights is always 900. To customise how Nightwind inverts colors by default, see how to set up a custom color scale
If you add your custom colors in tailwind.config.js using number notation, Nightwind will treat them the same way as Tailwind's colors when switching into dark mode.
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
primary: {
50: "#caf0f8", // becomes primary-900 in dark mode
300: "#90e0ef", // becomes primary-600 in dark mode
600: "#0077b6", // becomes primary-300 in dark mode
900: "#03045e", // becomes primary-50 in dark mode
},
},
},
},
}Check out color mappings to see how to further customize your dark theme.
Variants and color classes
Variants and other color classes can be enabled for Nightwind like so:
// tailwind.config.js
module.exports = {
// ...
theme: {
nightwind: {
colorClasses: [
"gradient",
"ring",
"ring-offset",
"divide",
"placeholder",
],
},
},
variants: {
nightwind: ["focus"], // Add any Tailwind variant
},
// ...
}The 'gradient' color class enables Nightwind for the 'from', 'via' and 'to' classes, allowing automatic dark gradients.
The 'nightwind-prevent' class
If you want an element to remain exactly the same in both light and dark modes, you can achieve this in Nightwind by adding a 'nightwind-prevent' class to the element.
Note: if you only want some of the colors to remain unchanged, consider using overrides.
To prevent all children of an element to remain unchanged in dark mode, you can add the 'nightwind-prevent-block' class to the element. All descandant nodes of the element will be prevented from s[...]
You can customize the name of both classes in your tailwind.config.js file
// tailwind.config.js
module.exports = {
theme: {
nightwind: {
fixedClass: "prevent-switch", // default 'nightwind-prevent'
fixedBlockClass: "prevent-switch-block", // default 'nightwind-prevent-block'
},
},
}Note: The 'nightwind-prevent' class doesn't work with @apply, so always add it in the html.
Transitions
Nightwind by default applies a '300ms' transition to all color classes. You can customize this value in your tailwind.config.js file, through the transitionDuration property.
// tailwind.config.js
module.exports = {
theme: {
nightwind: {
transitionDuration: "500ms", // default '300ms'
},
},
}If you wish to disable transition for a single class, you can add the 'duration-0' class to the element (it's already included in Nightwind).
If you wish to disable the generation of all transition classes, you can do so by setting the same value to false.
// tailwind.config.js
module.exports = {
theme: {
nightwind: {
transitionDuration: false, // default '300ms'
},
},
}Transition Classes
Nightwind by default generates transition classes for 'text', 'bg' and 'border' color classes. This should make most elements transition smoothly without affecting performances.
In your configuration file you can also set the transitionClasses property to 'full' to enable generation of transition classes for all color classes used throughout your website (i.e. rings, divi[...]
// tailwind.config.js
module.exports = {
theme: {
nightwind: {
transitionClasses: "full", // default ['text, 'bg', 'border']
},
},
}Alternatively, you can also specify which color classes you'd like to generate transition classes for.
// tailwind.config.js
module.exports = {
theme: {
nightwind: {
transitionClasses: ["bg", "ring"], // default ['text, 'bg', 'border']
},
},
}Custom color scale
This configuration allows you to define how one or more color weights convert in dark-mode. Note that these affects all color classes.
For example, you could make all -100 colors switch into -900 colors like so.
// tailwind.config.js
module.exports = {
theme: {
nightwind: {
colorScale: {
100: 900, // default 800
},
},
},
}Note: These settings can still be overridden for specific colors using color mappings, or in specific elements with overrides
Reduced preset
This preset simulates how Nightwind would behave without the -50 color classes. Any -50 color will essentially appear the same as -100 colors (both becomes -900)
This behaviour may be desirable for two main reasons:
- Makes the reversed -800 and -900 colors darker and more different between themselves.
- -500 colors remain the same in both dark and light mode
// tailwind.config.js
module.exports = {
theme: {
nightwind: {
colorScale: {
preset: "reduced",
},
},
},
}This is the corresponding scale:
// tailwind.config.js
colorScale: {
50: 900,
100: 900,
200: 800,
300: 700,
400: 600,
500: 500,
600: 400,
700: 300,
800: 200,
900: 100,
},Note: When using a preset, specific weights will be ignored.
Important selector
If you're using the important ID selector strategy in your config, such as
// tailwind.config.js
module.exports = {
important: "#app",
}Please note that Nightwind assumes that the #app element is a parent of the element which contains the toggled 'dark' and 'nightwind' classes.
If you're applying the 'important ID selector' to the same element that contains both the 'nightwind' and the toggled 'dark' classes (typically the root element), enable the following setting:
// tailwind.config.js
module.exports = {
theme: {
nightwind: {
importantNode: true,
},
},
}Note: Overrides will stop working as they always assume a parent-child relationship between elements.
Color mappings
Color mappings allow you to fine-tune your dark theme, change colors in batch and control how Nightwind behaves in dark mode. You set them up like this:
// tailwind.config.js
module.exports = {
theme: {
nightwind: {
colors: {
// Color mappings go here
},
},
},
}There are two main ways to map colors in Nightwind: using individual colors or color classes.
Syntax
You can use the following syntax to specify colors:
- Individual colors: in hex '#fff', Tailwind-based color codes 'red.100', or using CSS variables 'var(--primary)'
- Color classes: such as 'red' or 'gray'
Individual colors
You can use this to set individual dark colors, directly from tailwind.config.js
// tailwind.config.js
module.exports = {
theme: {
nightwind: {
colors: {
white: "gray.900",
black: "gray.50",
red: {
100: "#1E3A8A", // or 'blue.900'
500: "#3B82F6", // or 'blue.500'
900: "#DBEAFE", // or 'blue.100'
},
primary: "var(--secondary)",
secondary: "var(--primary)",
},
},
},
}- When a mapping is not specified, Nightwind will fallback to the default dark color (red-100 becomes #1E3A8A, while red-200 becomes red-700)
Note: Contrarily to all other cases, when you individually specify a dark color this way nightwind doesn't automatically invert the color weight. The same is also valid for overrides.
Color classes
This is useful when you want to switch a whole color class in one go. Consider the following example:
// tailwind.config.js
module.exports = {
theme: {
nightwind: {
colors: {
red: "blue",
yellow: "primary",
pink: "yellow.500",
},
},
extend: {
colors: {
primary: {
50: "#caf0f8",
300: "#90e0ef",
600: "#0077b6",
900: "#03045e",
},
},
},
},
}- All red color classes become blue in dark mode, with inverted weight (red-600 becomes blue-300);
- Yellow colors in dark mode will switch to the 'primary' custom color with inverted weights, when available (yellow-300 becomes primary-600, but yellow-200 becomes yellow-700)
- Notably, if you map a color class such as 'pink' to an individual color such as 'yellow.500', all pink color classes will become yellow-500 regardless of the color weight.
Hybrid mapping
You can even specify a default dark color for a color class, as well as individual colors for specific weights. You can do so by specifying a default value for a color class.
// tailwind.config.js
module.exports = {
theme: {
nightwind: {
colors: {
rose: {
default: "blue",
200: "yellow.300",
},
},
},
},
}Overrides
The dark: prefix (v2.3.0+)
By default, Nightwind automatically inverts all color classes. However, you may want to force a specific color in dark mode for a particular element, bypassing the automatic inversion.
You can achieve this by using the standard Tailwind dark: prefix. When Nightwind detects a dark: color class, it will apply that exact color in dark mode instead of calculating an inverted version.
<!-- Automatically inverted (bg-blue-600 becomes bg-blue-300 in dark mode) -->
<div class="bg-blue-600"></div>
<!-- Forced color (stays red-500 in dark mode) -->
<div class="bg-blue-600 dark:bg-red-500"></div>This acts as a "manual override" for developers who need fine-grained control over specific components while still enjoying the benefits of automatic dark mode elsewhere.
Typography
Nightwind provides out-of-the-box support for the official @tailwindcss/typography plugin.
When the prose class is used, Nightwind automatically handles the transitions and applies the prose-invert behavior (calculating the inverted colors for your typography) when dark mode is enabled.
<article class="prose">
<h1>My Awesome Title</h1>
<p>This will be automatically inverted in dark mode.</p>
</article>You can enable/disable typography support in your tailwind.config.js:
// tailwind.config.js
module.exports = {
theme: {
nightwind: {
typography: true, // enabled by default
},
},
}Tailwind CSS v4 Support
Nightwind is compatible with Tailwind CSS v4 through its compatibility layer.
How to use Nightwind with v4
To use Nightwind in a Tailwind v4 project, you must maintain a tailwind.config.js (or .ts) file to define your theme colors and plugins. This is because Nightwind's engine needs to iterate over the JavaScript theme object to automatically generate dark mode classes.
// tailwind.config.js
module.exports = {
darkMode: "class",
theme: {
extend: {
// Your theme here
}
},
plugins: [require("@thiagormoreira/nightwind")]
}In your main CSS file, import Tailwind and your config:
@import "tailwindcss";
@config "../tailwind.config.js";Known Limitations
- Native CSS-only Theme: Currently, Nightwind does not support themes defined exclusively within v4's new
@themeCSS blocks. If you migrate your colors entirely to CSS variables without a backing JS configuration, Nightwind will not be able to detect and invert them automatically. - Support Strategy: We are actively monitoring Tailwind v4's internal APIs to provide native, CSS-first support in a future major release.
