- No flicker.
- Framework agnostic. Supports React | Vue | Svelte.
react-perfect-dark-mode
gatsby-plugin-perfect-dark-mode
next-plugin-perfect-dark-mode
vue-perfect-dark-mode
- Svelte - This was written for Svelte :).
window.__pdm__.mode
is aWritable
.
- Tiny, less than 1kb.
- Works with server side rendering.
- Uses system color mode when JS is disabled.
- Listens for changes to the system color mode.
- Allows user to override the system color mode and save their preference.
- Allows clearing the saved preference and falling back to the system mode.
- Supports any number of color modes, not just
light
anddark
. - Syncs across tabs.
- Built for the web.
Created by @atomarranger
Examples
These are plain JavaScript examples of using perfect-dark-mode
, if you’re using a framework like React/Vue/Svelte then you can skip to that documentation.
Label
Display a label showing the mode.
const { mode } = window.__pdm__
// Get some elements we will use.
const labelEls = document.querySelectorAll('.pdm-label')
// Listen to the color mode and update the UI.
mode.subscribe((m) => labelEls.forEach((el) => (el.textContent = m)))
// At this point we can show the elements.
labelEls.forEach((el) => (el.style.visibility = 'unset'))
Emoji
Display an emoji showing the mode.
const { mode } = window.__pdm__
// Get some elements we will use.
const emojiEls = document.querySelectorAll('.pdm-emoji')
// Listen to the color mode and update the UI.
mode.subscribe((m) =>
emojiEls.forEach((el) => (el.className = `pdm-emoji emoji emoji-${m}`)),
)
// At this point we can show the elements.
emojiEls.forEach((el) => (el.style.visibility = 'unset'))
Toggle
You can toggle the color mode.
const { mode } = window.__pdm__
// Get some elements we will use.
const toggleEls = document.querySelectorAll('.pdm-toggle')
// These elements will toggle between light and dark modes.
toggleEls.forEach((el) => {
el.addEventListener('click', (e) =>
mode.update((v) => (v === 'light' ? 'dark' : 'light')),
)
})
Select
You can use a select for the color mode.
Perfect Dark Mode allows for more than just light
and dark
color modes.
On initialization PDM will read a list of modes from the <html>
element’s data-pdm
attribute.
These become available on .modes
.
.modes
is a Writable
like .mode
, so it can also be set
or update
ed.
const { mode, modes } = window.__pdm__
// We can use a select element for choosing a color mode
// instead of a toggle button.
const selectEls = document.querySelectorAll('.pdm-select')
// Update the select options with the modes.
modes.subscribe((modes) => {
selectEls.forEach((el) => {
// Preserve the current selection by saving then re-assigning.
const prevValue = el.value
el.innerHTML = modes
.map(
(m) =>
`<option value="${m}">${
m.charAt(0).toUpperCase() + m.slice(1)
}</option>`,
)
.join('\n')
el.value = prevValue
})
})
// Set the mode on change.
selectEls.forEach((el) =>
el.addEventListener('change', (e) => mode.set(e.target.value)),
)
// Set the value to mode.
mode.subscribe((m) => selectEls.forEach((el) => (el.value = m)))
Cycle
You can use the .update
method to cycle through color modes.
const { mode } = window.__pdm__
// Get some elements we will use.
const cycleEls = document.querySelectorAll('.pdm-cycle')
// These elements will cycle through all modes.
cycleEls.forEach((el) => {
el.addEventListener('click', (e) =>
mode.update(
(mode, modes, modeIndex) => modes[(modeIndex + 1) % modes.length],
),
)
})
Reset
You can reset the color mode and fallback to system color mode.
const { mode } = window.__pdm__
// Get some elements we will use.
const resetButtonEls = document.querySelectorAll('.pdm-reset')
// These elements will clear the saved color mode,
// which will cause the color mode to fallback to
// the system color mode.
resetButtonEls.forEach((el) =>
el.addEventListener('click', () => mode.set(undefined)),
)
Mode
For debugging/understanding here is the saved color mode and the system color mode.
The displayed color mode is SavedColorMode || SystemColorMode
.
Saved:
System:
const { modeSaved, modeOS } = window.__pdm__
// Show the saved mode for debugging.
const modeSavedEls = document.querySelectorAll('.pdm-mode-saved')
modeSaved.subscribe((v) =>
modeSavedEls.forEach((el) => (el.textContent = `Saved Color Mode: ${v}`)),
)
// Show the system mode for debugging.
const modeSystemEls = document.querySelectorAll('.pdm-mode-system')
modeOS.subscribe((v) =>
modeSystemEls.forEach((el) => (el.textContent = `System Color Mode: ${v}`)),
)
Animating Transitions
You should add the transition property with JS to avoid a transition between no-js and js.
// We want to make sure that if the system mode is 'light'
// and the saved mode is 'dark' that we do not transition
// between the two, so we add the transition after a frame
// has passed.
requestAnimationFrame(() =>
requestAnimationFrame(
() =>
(document.documentElement.style.transition =
'background 0.5s, color 0.5s'),
),
)
Toggle with System Mode
This is a toggle that includes showing when we are using the system color mode.
const { modeOS, modeSaved } = window.__pdm__
// We can build a Readable using modeOS and modeSaved.
// With Svelte you could use derived: https://svelte.dev/docs#derived
const combinedMode = (() => {
const listeners = new Set()
let os
let saved
let previous
const onChange = () => {
const newValue = saved !== undefined ? saved : `System (${os})`
if (previous !== newValue) {
listeners.forEach((cb) => cb(newValue))
previous = newValue
}
}
modeOS.subscribe((v) => {
os = v
onChange()
})
modeSaved.subscribe((v) => {
saved = v
onChange()
})
onChange()
return {
subscribe(listener) {
listeners.add(listener)
listener(previous)
return () => listeners.delete(listener)
},
}
})()
// Get label elements.
const labelEls = document.querySelectorAll('.pdm-label-with-system-mode')
// Update the label elements with the mode.
combinedMode.subscribe((m) => labelEls.forEach((el) => (el.textContent = m)))
// At this point we can show the elements.
labelEls.forEach((el) => (el.style.visibility = 'unset'))
// Get the button elements.
const buttonEls = document.querySelectorAll('.pdm-toggle-with-system-mode')
// These buttons will alternate through light, dark, and system modes.
buttonEls.forEach((el) => {
el.addEventListener('click', (e) =>
modeSaved.update((v) => {
if (v === 'light') {
return 'dark'
}
if (v === 'dark') {
return undefined
}
if (v === undefined) {
return 'light'
}
}),
)
})
perfect-dark-mode
Installation
There are a few options for installing perfect-dark-mode
.
Note, if you use Gatsby or Next.js you do not need to do this.
Yarn
yarn add perfect-dark-mode
Then you must add node_modules/perfect-dark-mode/dist/index.js
as a script in the <head>
of your page.
How you do this will depend on the framework you are using.
UNPKG
Add this code to the <head>
of your page:
<script src="https://unpkg.com/perfect-dark-mode@1.0.0/dist/index.js"></script>
Copy and Paste
Add this code to the <head>
of your page:
<script>(()=>{var W=({prefix:r="pdm",modes:w=["light","dark"]}={})=>{var n=r,l=window.localStorage,t=w,c=new Set,h=e=>{t=e,c.forEach(o=>o(e))},O={subscribe(e){return e(t),c.add(e),()=>c.delete(e)},set:h,update(e){h(e(t))}},u=new Set,a=matchMedia("(prefers-color-scheme: dark)"),m,f=({matches:e})=>{m=e?"dark":"light",u.forEach(o=>o(m))};a.addEventListener?a.addEventListener("change",f):a.addListener(f),f(a);var v={subscribe(e){return e(m),u.add(e),()=>u.delete(e)}},P=e=>{if(!(!e||!t.includes(e)))return e},p=new Set,s=P(l.getItem(n)),M=(e,o=!0)=>{e!==s&&(o&&(e!==void 0?l.setItem(n,e):l.removeItem(n)),p.forEach(T=>T(e)),s=e)};window.addEventListener("storage",e=>e.key===n&&M(e.newValue||void 0,!1));var i={subscribe(e){return e(s),p.add(e),()=>p.delete(e)},set:M,update(e){M(e(s))}},g,k,d,b=new Set,x=()=>{var e=g||k;e!==d&&(d=e,b.forEach(o=>o(d)))};i.subscribe(e=>{g=e,x()}),v.subscribe(e=>{k=e,x()});var E={subscribe(e){return e(d),b.add(e),()=>b.delete(e)},set:i.set,update(e){var o=t.indexOf(d);o=o===-1?0:o,i.set(e(d,t,o))}},C=document.documentElement.classList,S;return E.subscribe(e=>{S&&C.remove(`${r}-${S}`),C.add(`${r}-${e}`),S=e}),C.add(r),{mode:E,modes:O,modeOS:v,modeSaved:i}};window.__pdm__=W({modes:document.documentElement.dataset.pdm?.split(" ")});})();</script>
Usage
A class indicating the color mode will be added to <html>
(e.g. pdm-light
or pdm-dark
).
This is done before the rest of your page is rendered (that’s why it needs to be in head).
This does:
- Determine the correct color mode when the page is loaded.
- Save changes to the mode.
- Allow for listening to the mode and building controls that depend on it.
This does not:
- Handle styling for you.
- Styling should be done using CSS variables.
- Automatically convert your page to dark mode.
- This would be error prone, it is better to intentionally design your color modes using CSS variables.
- Provide UI components for you.
- This page does show some examples of how to make simple controls in various frameworks that listen to the mode.
Example CSS
Here is a simple implementation of dark and light modes using CSS variables and the classes added by PDM:
/* This supports users with JS disabled. */
@media (prefers-color-scheme: dark) {
:root {
--color: white;
--background: black;
}
}
/* This supports users with JS disabled. */
@media (prefers-color-scheme: light) {
:root {
--color: black;
--background: white;
}
}
/* Styles for when light mode is enabled. */
.pdm-light {
--color: black;
--background: white;
}
/* Styles for when dark mode is enabled. */
.pdm-dark {
--color: white;
--background: black;
}
/* Default color and background. */
/* If you add a color or background on other components (e.g. body or some custom Button) */
/* that will override these. You will need to change those styles to use these CSS variables. */
:root {
color: var(--color);
background: var(--background);
}
In the rest of your app use --color
and --background
as needed.
Listening
- You can
subscribe
to the mode, this can be used for rendering a toggle component. - The first call of your listener is synchronous so you can get the value before rendering.
const { mode } = window.__pdm__
const unsubscribe = mode.subscribe((v) => console.log(v))
Setting
- You can
set
the mode. - You can
update
the mode based on the current mode.
const { mode } = window.__pdm__
mode.set('light')
mode.update((mode) => (mode === 'light' ? 'dark' : 'light'))
API Reference
window.__pdm__
mode: Writable<ColorMode>
- The resolved mode,
modeSaved || modeOS
. - Can be set or updated.
subscribe(listener: (mode: ColorMode) => void): () => void
set(mode: ColorMode): void
update(updater: (mode: ColorMode, modes: ColorMode[], index: number | undefined) => ColorMode): void
- The update function gives you the current modes and the current mode index so you can cycle
through by returning
modes[(modeIndex + 1) % modes.length]
.
- The update function gives you the current modes and the current mode index so you can cycle
through by returning
- The resolved mode,
modes: Writable<ColorMode[]>
- Valid color modes, can be used to render a list.
- Can be set or updated.
subscribe(listener: (modes: ColorMode[]) => void): () => void
set(modes: ColorMode[]): void
update(updater: (modes: ColorMode[]) => ColorMode[]): void
modeSaved: Writable<ColorMode>
- This is mainly for debugging, prefer using
mode
. subscribe(listener: (mode: ColorMode) => void): () => void
- This is mainly for debugging, prefer using
modeOS: Readable<ColorMode>
- This is mainly for debugging, prefer using
mode
. - The system mode cannot be written by JS, it can be updated by the user in their system settings.
- We do listen for changes to the system color mode.
subscribe(listener: (mode: ColorMode) => void): () => void
- This is mainly for debugging, prefer using
Pure Usage
If for some reason you don’t want PDM to automatically initialize itself and add itself on window.__pdm__
you can use the pure version:
import { createPerfectDarkMode } from 'perfect-dark-mode'
const pdm = createPerfectDarkMode()
react-perfect-dark-mode
Installation
You must first install perfect-dark-mode
into the <head>
of your document.
yarn add react-perfect-dark-mode
Usage
In a component you can use the hook:
import React from 'react'
import { usePerfectDarkMode } from 'react-perfect-dark-mode'
export const Toggle = () => {
const { mode, updateMode } = usePerfectDarkMode()
return (
<button
style={{ visibility: mode !== undefined ? 'visible' : 'hidden' }}
onClick={() =>
updateMode(
(mode, modes, modeIndex) => modes[(modeIndex + 1) % modes.length],
)
}
>
{mode}
</button>
)
}
gatsby-plugin-perfect-dark-mode
This plugin makes it easy to add perfect-dark-mode
to Gatsby.
Installation
You do not need to add perfect-dark-mode
to <head>
like you do for react-perfect-dark-mode
.
This plugin puts perfect-dark-mode
in <head>
for you.
yarn add gatsby-plugin-perfect-dark-mode
Add gatsby-plugin-perfect-dark-mode
to your gatsby-config.js
file.
Usage
In a component you can use the hook:
import React from 'react'
import { usePerfectDarkMode } from 'gatsby-plugin-perfect-dark-mode'
export const Toggle = () => {
const { mode, updateMode } = usePerfectDarkMode()
return (
<button
style={{ visibility: mode !== undefined ? 'visible' : 'hidden' }}
onClick={() =>
updateMode(
(mode, modes, modeIndex) => modes[(modeIndex + 1) % modes.length],
)
}
>
{mode}
</button>
)
}
next-plugin-perfect-dark-mode
This plugin makes it easy to add perfect-dark-mode
to Next.js.
Installation
You do not need to add perfect-dark-mode
to <head>
like you do for react-perfect-dark-mode
.
This plugin provides InjectPerfectDarkMode
to do that.
yarn add next-plugin-perfect-dark-mode
You must render InjectPerfectDarkMode
on any page you use it on.
import Head from 'next/head'
import { InjectPerfectDarkMode } from 'next-plugin-perfect-dark-mode'
// In the Next.js blog starter you would add this to pages/index.js
// In other setups it would probably make sense to add to wherever your SEO component is.
export default function Home() {
return (
<div className="container">
<Head>
<title>Create Next App</title>
<link rel="icon" href="/favicon.ico" />
</Head>
{/* Add this line. */}
<InjectPerfectDarkMode />
{/* The rest... */}
</div>
)
}
Usage
In a component you can use the hook:
import React from 'react'
import { usePerfectDarkMode } from 'next-plugin-perfect-dark-mode'
export const Toggle = () => {
const { mode, updateMode } = usePerfectDarkMode()
return (
<button
style={{ visibility: mode !== undefined ? 'visible' : 'hidden' }}
onClick={() =>
updateMode(
(mode, modes, modeIndex) => modes[(modeIndex + 1) % modes.length],
)
}
>
{mode}
</button>
)
}
vue-perfect-dark-mode
This integration is for Vue 3.
Installation
You must first install perfect-dark-mode
into the <head>
of your document.
yarn add vue-perfect-dark-mode
Usage
In a component you can use the hook:
<script>
import { usePerfectDarkMode } from 'vue-perfect-dark-mode'
export default {
name: 'App',
setup(props) {
const { mode, updateMode } = usePerfectDarkMode()
return {
mode,
onClick() {
updateMode(
(mode, modes, modeIndex) => modes[(modeIndex + 1) % modes.length],
)
},
}
},
}
</script>
<template>
<button :class="{ visible: mode !== undefined }" @click="onClick">
{{ mode }}
</button>
</template>
<style>
button {
visibility: hidden;
}
.visible {
visibility: visible;
}
</style>
Join my Newsletter
I'll send you an email when I have something interesting to show.