Skip to Content
Delmare Digital
Guides

Dark mode

How the editor follows Payload's dark mode, the preview light and dark toggle, and detecting the preview theme in components.

The Puck editor detects when Payload's admin is in dark mode and adjusts its own UI to stay readable. Separately, a toggle lets you preview the page itself in light or dark mode.

How it works

  1. Editor UI. The editor detects dark mode from Payload's data-theme attribute (the theme chosen in Payload's account settings), a .dark class, or the OS prefers-color-scheme preference, then injects overrides for Puck's CSS variables.
  2. Preview iframe. A sun/moon toggle switches the preview content between light and dark, independently of the editor UI.

Configuration

Both are on by default. Change them with props on PuckEditor:

<PuckEditor
  autoDetectDarkMode={true}          // Follow Payload's dark mode (default: true)
  showPreviewDarkModeToggle={true}   // Show the light/dark toggle (default: true)
  initialPreviewDarkMode={false}     // Start the preview in light mode (default: false)
/>

Building your own editor

For a custom editor implementation, use the pieces directly:

components/CustomEditor.tsx
'use client'
import { useState } from 'react'
import {
  DarkModeStyles,
  PreviewModeToggle,
  useDarkMode,
} from '@delmaredigital/payload-puck/editor'

function CustomEditor() {
  const { isDarkMode, source } = useDarkMode()
  const [previewDark, setPreviewDark] = useState(false)

  return (
    <>
      <DarkModeStyles />
      <PreviewModeToggle
        isDarkMode={previewDark}
        onToggle={setPreviewDark}
      />
      <Puck ... />
    </>
  )
}

useDarkMode() returns isDarkMode and source, which is 'attribute', 'class', 'media-query' or 'none'.

Detecting the preview theme in components

CSS dark-mode variants, such as Tailwind's dark: classes, work in the preview automatically through the data-theme attribute. Values computed in JavaScript, such as an overlay colour, don't update when the toggle changes. For those, read usePuckPreviewTheme(), which updates reactively. It returns null outside the editor:

hooks/useDetectTheme.ts
import { usePuckPreviewTheme } from '@delmaredigital/payload-puck/editor'
import { useState } from 'react'

function useDetectTheme() {
  const puckTheme = usePuckPreviewTheme()

  // On the frontend (outside the editor), read from the DOM
  const [domTheme] = useState(() =>
    typeof document !== 'undefined'
      ? document.documentElement.getAttribute('data-theme') === 'dark'
      : false
  )

  // In the editor: use context. On the frontend: use the DOM.
  return puckTheme !== null ? puckTheme : domTheme
}

A layout can also start its preview dark with editorDarkMode: true; see Layouts.

Edit this page on GitHub

On this page