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

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

Version Size Build Codecov

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:

This does not:

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

const { mode } = window.__pdm__
const unsubscribe = mode.subscribe((v) => console.log(v))

Setting

const { mode } = window.__pdm__
mode.set('light')
mode.update((mode) => (mode === 'light' ? 'dark' : 'light'))

API Reference

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

Version Size

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

Version Size

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

Version Size

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

Version Size

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.