Gatsby: Dark mode support in gatsby-plugin-styled-components

Created on 22 Apr 2020  路  4Comments  路  Source: gatsbyjs/gatsby

The way gatsby-plugin-styled-components currently implements SSR allows for only one style sheet per pathname.

import React from 'react'
import { ServerStyleSheet, StyleSheetManager } from 'styled-components'

const sheetByPathname = new Map()

exports.wrapRootElement = ({ element, pathname }) => {
  const sheet = new ServerStyleSheet()
  sheetByPathname.set(pathname, sheet)
  return <StyleSheetManager sheet={sheet.instance}>{element}</StyleSheetManager>
}

exports.onRenderBody = ({ setHeadComponents, pathname }) => {
  const sheet = sheetByPathname.get(pathname)
  if (sheet) {
    setHeadComponents([sheet.getStyleElement()])
    sheetByPathname.delete(pathname)
  }
}

This is a problem if you're trying to implement a dark mode without applying extra CSS classes to your components. In my case, dark mode is implemented by styled-components' ThemeProvider taking a theme function. This function receives the current color mode from a hook in gatsby-ssr's wrapRootElement:

import React from 'react'
import { useDarkMode } from 'hooks'
import { ThemeProvider } from 'styled-components'
import theme from 'utils/theme'

function Theme({ children }) {
  const [darkMode] = useDarkMode()
  return <ThemeProvider theme={theme(darkMode)}>{children}</ThemeProvider>
}

export const wrapRootElement = ({ element }) => <Theme>{element}</Theme>

Even though the hook persists the current color mode to local storage, this implementation leads to the light mode briefly flashing when hard reloading the page (cmd + shift + R) since the SSR stylesheet doesn't know about different styles for the dark theme.

To solve this I was thinking about how to override gatsby-plugin-styled-components's default behavior and server-render two stylesheets per pathname in order to serve the correct one depending on whether or not darkMode was found in local storage. If anyone can offer some advice on how to do that or if there might be a more elegant solution, that would be much appreciated! :)

help wanted design question or discussion

Most helpful comment

Hi Janosh!

I fear this isn't something we would directly bake into gatsby-plugin-styled-components and require the user to handle that. For a lengthy explanation you can read my colleagues blog post: https://joshwcomeau.com/gatsby/dark-mode/

TL;DR:

All 4 comments

I don't see an easy way to avoid this flash with static HTML. You will still see the static version with the default theme while js is loading/parsing/react hydrating, right? But maybe I am missing something?

@LekoArts Any ideas on this?

@vladar I was thinking maybe I could use the onRenderBody API to either change the SSR stylesheet in place or override it with a second one before the page paints. Something like

import React from 'react'

const noflash = `(function() { try {
  if (localStorage.getItem('colorMode') === 'dark') {
    // change or replace style sheet here
  }
} catch (e) {} })();`

export const onRenderBody = ({ setPreBodyComponents }) => {
  setPreBodyComponents(<script>{noflash}</script>)
}

Hi Janosh!

I fear this isn't something we would directly bake into gatsby-plugin-styled-components and require the user to handle that. For a lengthy explanation you can read my colleagues blog post: https://joshwcomeau.com/gatsby/dark-mode/

TL;DR:

Great post by Josh Comeau! I adapted his implementation. Super happy with it.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

timbrandin picture timbrandin  路  3Comments

jimfilippou picture jimfilippou  路  3Comments

Oppenheimer1 picture Oppenheimer1  路  3Comments

andykais picture andykais  路  3Comments

rossPatton picture rossPatton  路  3Comments