Skip to content

Instantly share code, notes, and snippets.

@DylanVann
Last active October 7, 2020 15:43
Show Gist options
  • Select an option

  • Save DylanVann/c46ad8f11eac6d5fe16ed04abf84459e to your computer and use it in GitHub Desktop.

Select an option

Save DylanVann/c46ad8f11eac6d5fe16ed04abf84459e to your computer and use it in GitHub Desktop.
Color mode switcher in vanilla JS.
const g=()=>{let l=!1;const s="color-mode",h=["light","dark"],C=o=>h.includes(o)?o:"light",n=o=>o?"dark":"light",e=matchMedia("(prefers-color-scheme: dark)"),i=n(e.matches),u="light",d=localStorage.getItem(s);d!==null&&(l=!0);let c=u;d?c=C(d):i&&(c=i);let t;const r=(o,f)=>{document.body.classList.remove(t),document.body.classList.add(o),f&&(localStorage.setItem(s,o),l=!0),t=o,a.forEach(m=>m(o))},a=new Set,M=o=>{l||r(n(o.matches),!1)};return e.addEventListener?e.addEventListener("change",M):e.addListener(M),document.addEventListener("DOMContentLoaded",()=>r(c,!1)),{subscribe:o=>(o(t),a.add(o),()=>a.delete(o)),set:o=>{r(o,!0)},update:o=>{r(o(t),!0)}}},S=g();window.__color_mode__=S;

This code will add either a dark or a light class to your body element.

To style your color modes you can use CSS variables, e.g.

body.light {
  --background-color: white;
  --text-color: black;
}

body.dark {
  --background-color: black;
  --text-color: white;
}

Then in the rest of your app use --text-color and --background-color as needed.

The color mode is initially determined by checking the system color mode using matchMedia('(prefers-color-scheme: dark)').

After that any change to the color mode made using .set will save the mode to localStorage, where it will be read from in the future.

You should place the JS inside your <head> tag so that it is run before anything is shown on the page.

Listening:

// You can listen to color mode, this can be used in React or Svelte to render a toggle component.
const unsubscribe = window.__color_mode__.subscribe(v => console.log(v))

Setting:

// You can set the color mode.
window.__color_mode__.set('light')
// Or use update to set based on the current mode.
window.__color_mode__.update(mode => mode === 'light' ? 'dark' : 'light')
/** Callback to inform of a value updates. */
declare type Subscriber<T> = (value: T) => void
/** Unsubscribes from value updates. */
declare type Unsubscriber = () => void;
/** Callback to update a value. */
declare type Updater<T> = (value: T) => T;
/** Readable interface for subscribing. */
export interface Readable<T> {
/**
* Subscribe on value changes.
* @param run subscription callback
* @param invalidate cleanup callback
*/
subscribe(run: Subscriber<T>): Unsubscriber;
}
/** Writable interface for both updating and subscribing. */
export interface Writable<T> extends Readable<T> {
/**
* Set value and inform subscribers.
* @param value to set
*/
set(value: T): void;
/**
* Update value using callback and inform subscribers.
* @param updater callback
*/
update(updater: Updater<T>): void;
}
type ColorMode = 'light' | 'dark'
type ColorModeSwitcher = Writable<ColorMode>
const createColorModeSwitcher = (): ColorModeSwitcher => {
let hasSavedMode = false
const colorModeKey = 'color-mode'
const colorModes = ['light', 'dark']
const parseColorMode = (mode: string): ColorMode =>
colorModes.includes(mode) ? (mode as ColorMode) : 'light'
const matchesToMode = (matches: boolean): ColorMode =>
matches ? 'dark' : 'light'
const mediaQuery = matchMedia('(prefers-color-scheme: dark)')
const colorModeOS = matchesToMode(mediaQuery.matches)
const colorModeDefault = 'light'
const colorModeSaved: ColorMode | null = localStorage.getItem(
colorModeKey,
) as ColorMode
if (colorModeSaved !== null) {
hasSavedMode = true
}
let colorMode: ColorMode = colorModeDefault
if (colorModeSaved) {
colorMode = parseColorMode(colorModeSaved)
} else if (colorModeOS) {
colorMode = colorModeOS
}
let m: ColorMode
const internalSet = (mode: ColorMode, save: boolean) => {
document.body.classList.remove(m)
document.body.classList.add(mode)
if (save) {
localStorage.setItem(colorModeKey, mode)
hasSavedMode = true
}
m = mode
listeners.forEach((l) => l(mode))
}
const listeners = new Set<Function>()
const onChange = (e) => {
if (!hasSavedMode) {
internalSet(matchesToMode(e.matches), false)
}
}
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', onChange)
} else {
mediaQuery.addListener(onChange)
}
document.addEventListener('DOMContentLoaded', () =>
internalSet(colorMode, false),
)
return {
subscribe: (listener) => {
listener(m)
listeners.add(listener)
return () => listeners.delete(listener)
},
set: (mode: ColorMode) => {
internalSet(mode, true)
},
update: (updater) => {
internalSet(updater(m), true)
},
}
}
const colorModeSwitcher = createColorModeSwitcher()
declare global {
interface Window {
__color_mode__: ColorModeSwitcher
}
}
window.__color_mode__ = colorModeSwitcher
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment