January 2024

Dark mode in WebGL

Using matchMedia

Use matchMedia and listen for changes to get updates when dark mode is toggled.

import { useEffect, useState } from "react"

export const useDarkMode = () => {
  const [isDarkMode, setIsDarkMode] = useState(
    window.matchMedia('(prefers-color-scheme: dark)').matches
  )

  useEffect(() => {
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
    const handleChange = (event: MediaQueryListEvent) => {
      setIsDarkMode(event.matches)
    }

    mediaQuery.addEventListener('change', handleChange)

    return () => {
      mediaQuery.removeEventListener('change', handleChange)
    }
  }, [])

  return isDarkMode
}

const ExampleOne = () => {
  const darkMode = useDarkMode()

  return (
    <Canvas>
      <TorusKnot color={darkMode ? 'cyan' : 'hotpink'} />
    </Canvas>
  )
}

Add CSS Custom Properties

CSS Custom Properties are a powerful tool and are easy to consume in CSS, but it gets a bit trickier when you want to use them outside of CSS. We can create another hook which will extract custom properties and update when dark mode is toggled

const extractCustomProperties = (element: Element, properties: string[]) => {
  const style = global.getComputedStyle(element)
  return properties.reduce((previous, current) => {
    previous[current] = style.getPropertyValue(current) || null
    return previous
  }, {} as Record<string, string | null>)
}

export const useCustomProperties = <K extends string>(
  properties: K[]
): {
  ref: React.Ref<HTMLElement>
  customProperties: Record<K, string | null>
  CustomProperties: React.FC<React.HTMLAttributes<HTMLDivElement>>
} => {
  const darkMode = useDarkMode()
  const elRef = useRef<HTMLElement>(null)
  const [customProperties, setCustomProperties] = useState(
    {} as Record<K, string | null>
  )

  useEffect(() => {
    if (!elRef.current) return

    setCustomProperties(
      extractCustomProperties(elRef.current, properties) as Record<
        K,
        string | null
      >
    )
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [darkMode, ...properties])

  const CustomProperties = ({
    children,
    ...restProps
  }: React.PropsWithChildren<React.HTMLAttributes<HTMLDivElement>>) => (
    <div ref={elRef as React.Ref<HTMLDivElement>} {...restProps}>
      {children}
    </div>
  )

  return useMemo(
    () => ({
      ref: elRef,
      customProperties,
      CustomProperties,
    }),
    [customProperties]
  )
}

const ExampleTwo = () => {
  const { CustomProperties, customProperties } = useCustomProperties(['--🎨-art-accent'])
  const color = customProperties['--🎨-art-accent'] || 'green'

  return (
    <CustomProperties>
      <Canvas>
        <TorusKnot color={color} />
      </Canvas>
    </CustomProperties>
  )
}