Skip to content

Dark mode optimization

Joy UI uses CSS variables to ensure that server-rendered apps can load in dark mode on first render.

Joy UI is optimized so that end users who select dark mode as their preferred color scheme never see a flash of light mode when the app first renders. This is a common problem for server-side-rendered (SSR) apps and sites built with static-site generators (SSGs).

To solve this problem, Joy UI uses CSS variables to render all color schemes at build time so that the user's preferred mode can be served to them on first load.

The problem: flickering on first load

In a server-rendered context, an app is built long before it reaches the client—which means that it can't account for the user's preferred color scheme when it first loads.

As a result, if you load such an app, switch to dark mode, and then refresh, you'll see a flash of the default light mode before client-side hydration kicks in and switches it back to dark mode. Indeed, this light-mode "flash" will occur every time you load up the app in the future, as long as your browser remembers that you prefer dark mode.

This can cause eye fatigue in a low-light setting, not to mention a frustrating interruption of the user experience—especially for those who interact with the app when it's in between modes.

The GIF below illustrates this problem:

An example video that shows a page that initially loads correctly in dark mode but quickly flickers to light mode.

The solution: CSS variables

Solving this problem required us to take a novel approach to styling and theming. (See this RFC on CSS variables support to learn more about the implementation of this feature.)

Thanks to Joy UI's built-in support for CSS variables, your app can render all of its color schemes at build time, so that the user's preference can be injected before the DOM is rendered in the browser.

Joy UI provides the getInitColorSchemeScript() function to make this flash-free dark mode possible with React frameworks like Next.js or Remix. This function must be placed before the main script so it can apply the correct stylesheet before your components are rendered.

The code snippet below shows how this works with the Next.js Pages Router:

import Document, { Html, Head, Main, NextScript } from 'next/document';
import { getInitColorSchemeScript } from '@mui/joy/styles';

export default class MyDocument extends Document {
  render() {
    return (
      <Html data-color-scheme="light">
          <Main />
          <NextScript />

See the Applying dark mode page for more details on usage with other frameworks.