Skip to main content
Theme UI provides built-in support for multiple color modes, including dark mode, with automatic persistence to localStorage and media query detection.

Basic Color Modes Setup

Define color modes in your theme:
const theme = {
  config: {
    initialColorModeName: 'light',
  },
  colors: {
    text: '#000',
    background: '#fff',
    primary: '#07c',
    modes: {
      dark: {
        text: '#fff',
        background: '#000',
        primary: '#0cf'
      }
    }
  }
}

The useColorMode Hook

Use the useColorMode hook to read and update the current color mode:
export function useColorMode<T extends string = string>(): [
  T,
  Dispatch<SetStateAction<T>>
]
Example:
import { useColorMode } from '@theme-ui/color-modes'

function ColorModeToggle() {
  const [colorMode, setColorMode] = useColorMode()
  
  return (
    <button
      onClick={() => {
        setColorMode(colorMode === 'light' ? 'dark' : 'light')
      }}
    >
      Toggle {colorMode === 'light' ? 'Dark' : 'Light'} Mode
    </button>
  )
}

Color Mode Provider

Wrap your app with ColorModeProvider:
import { ThemeProvider } from '@theme-ui/core'
import { ColorModeProvider } from '@theme-ui/color-modes'
import theme from './theme'

function App() {
  return (
    <ThemeProvider theme={theme}>
      <ColorModeProvider>
        {/* Your app */}
      </ColorModeProvider>
    </ThemeProvider>
  )
}

Configuration Options

initialColorModeName

Set the default color mode:
const theme = {
  config: {
    initialColorModeName: 'light'
  }
}
Do not use a key from theme.colors.modes as the initialColorModeName. It should be a unique name for your base color mode.

useColorSchemeMediaQuery

Detect the user’s system preference:
const theme = {
  config: {
    initialColorModeName: 'light',
    useColorSchemeMediaQuery: true  // or 'system' or 'initial'
  }
}
Options:
  • false (default): Don’t detect system preference
  • true or 'initial': Detect on initial load only
  • 'system': Always follow system preference with live updates
Example from Theme UI source:
const { initialColorModeName, useColorSchemeMediaQuery, useLocalStorage } =
  outerTheme.config || outerTheme

let [colorMode, setColorMode] = useState(() => {
  const preferredMode =
    useColorSchemeMediaQuery !== false && getPreferredColorScheme()

  return preferredMode || initialColorModeName
})

useLocalStorage

Persist color mode to localStorage:
const theme = {
  config: {
    useLocalStorage: true  // default
  }
}
Set to false to disable persistence:
const theme = {
  config: {
    useLocalStorage: false
  }
}
From the source code:
const storage = {
  get: () => {
    try {
      return window.localStorage.getItem('theme-ui-color-mode')
    } catch (err) {
      console.warn(
        'localStorage is disabled and color mode might not work as expected.',
        'Please check your Site Settings.',
        err
      )
    }
  },
  set: (value: string) => {
    try {
      window.localStorage.setItem('theme-ui-color-mode', value)
    } catch (err) {
      console.warn(
        'localStorage is disabled and color mode might not work as expected.',
        'Please check your Site Settings.',
        err
      )
    }
  },
}

printColorModeName

Set a specific color mode for printing:
const theme = {
  config: {
    initialColorModeName: 'dark',
    printColorModeName: 'light'  // Use light mode when printing
  }
}

useCustomProperties

Enable or disable CSS custom properties (CSS variables):
const theme = {
  config: {
    useCustomProperties: true  // default
  }
}
Set to false for legacy browser support (IE11):
const theme = {
  config: {
    useCustomProperties: false
  }
}

Multiple Color Modes

Define multiple color modes beyond just light and dark:
const theme = {
  config: {
    initialColorModeName: 'light'
  },
  colors: {
    text: '#000',
    background: '#fff',
    primary: '#07c',
    modes: {
      dark: {
        text: '#fff',
        background: '#000',
        primary: '#0cf'
      },
      sepia: {
        text: '#433422',
        background: '#f1e7d0',
        primary: '#d97706'
      },
      high-contrast: {
        text: '#000',
        background: '#fff',
        primary: '#00f'
      }
    }
  }
}
Toggle between modes:
function ColorModePicker() {
  const [colorMode, setColorMode] = useColorMode()
  
  return (
    <div>
      <button onClick={() => setColorMode('light')}>Light</button>
      <button onClick={() => setColorMode('dark')}>Dark</button>
      <button onClick={() => setColorMode('sepia')}>Sepia</button>
      <button onClick={() => setColorMode('high-contrast')}>High Contrast</button>
      <p>Current mode: {colorMode}</p>
    </div>
  )
}

Media Query Detection

Theme UI automatically detects the prefers-color-scheme media query:
const DARK_QUERY = '(prefers-color-scheme: dark)'
const LIGHT_QUERY = '(prefers-color-scheme: light)'

const getPreferredColorScheme = (): 'dark' | 'light' | null => {
  if (typeof window !== 'undefined' && window.matchMedia) {
    if (window.matchMedia(DARK_QUERY).matches) {
      return 'dark'
    }
    if (window.matchMedia(LIGHT_QUERY).matches) {
      return 'light'
    }
  }
  return null
}

Example: Complete Color Mode Setup

Here’s a complete example from the Theme UI source:
import { makeTheme } from '@theme-ui/css/utils'

export const theme = makeTheme({
  config: {
    initialColorModeName: 'light',
    useColorSchemeMediaQuery: true,
  },
  colors: {
    text: '#000',
    background: '#fff',
    primary: '#07c',
    secondary: '#b0b',
    modes: {
      dark: {
        text: '#fff',
        background: '#222',
        primary: '#0cf',
        secondary: '#faf',
      },
    },
  },
  fonts: {
    body: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
    heading: 'sans-serif',
  },
  styles: {
    root: {
      fontFamily: 'body',
      color: 'text',
      bg: 'background',
    },
  },
})

Accessing Color Values

When using CSS custom properties (default), colors are converted to CSS variables:
import { useThemeUI } from '@theme-ui/core'

function MyComponent() {
  const { theme } = useThemeUI()
  
  // theme.colors contains CSS custom properties
  console.log(theme.colors.primary)  // 'var(--theme-ui-colors-primary)'
  
  // Use rawColors to access original values
  console.log(theme.rawColors.primary)  // '#07c'
  
  return <div>Primary: {theme.rawColors.primary}</div>
}
When useCustomProperties is enabled, use theme.rawColors to access the original color values, as theme.colors will contain CSS custom property references.

Preventing Flash on Load

Theme UI includes a script to prevent color mode flash during SSR:
export const InitializeColorMode = () =>
  jsx('script', {
    key: 'theme-ui-no-flash',
    dangerouslySetInnerHTML: {
      __html: `(function() { try {
        var mode = localStorage.getItem('theme-ui-color-mode');
        if (!mode) return
        document.documentElement.classList.add('theme-ui-' + mode);
      } catch (e) {} })();`,
    },
  })
Add this to your document head in SSR frameworks:
// Next.js _document.js
import { InitializeColorMode } from '@theme-ui/color-modes'

export default function Document() {
  return (
    <Html>
      <Head>
        <InitializeColorMode />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}

TypeScript Support

Type-safe color mode with TypeScript:
type ColorMode = 'light' | 'dark' | 'sepia'

function ColorModeToggle() {
  const [colorMode, setColorMode] = useColorMode<ColorMode>()
  
  // colorMode is typed as 'light' | 'dark' | 'sepia'
  const nextMode: ColorMode = colorMode === 'light' ? 'dark' : 'light'
  
  return (
    <button onClick={() => setColorMode(nextMode)}>
      Current: {colorMode}
    </button>
  )
}

Build docs developers (and LLMs) love