TypeScript icon, indicating that this package has built-in type declarations

1.0.0 • Public • Published


Version Size Build Codecov

  • No flicker.
  • Framework agnostic. Supports React | Vue | Svelte.
  • 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 and dark.
  • Syncs across tabs.
  • Built for the web.


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


Add this code to the <head> of your page:

<script src=""></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>


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.


  • 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))


  • You can set the mode.
  • You can update the mode based on the current mode.
const { mode } = window.__pdm__
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].
    • 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
    • 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

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()



Package Sidebar


npm i perfect-dark-mode

Weekly Downloads






Unpacked Size

15.9 kB

Total Files


Last publish


  • dylanvann