diff --git a/.changeset/themeprovider-ssr-auto.md b/.changeset/themeprovider-ssr-auto.md
new file mode 100644
index 00000000000..a3c2e2eb7ea
--- /dev/null
+++ b/.changeset/themeprovider-ssr-auto.md
@@ -0,0 +1,5 @@
+---
+'@primer/react': patch
+---
+
+Fixes a bug for theming with server side rendering where the output of the server and client mismatch [#1773](https://github.com/primer/react/issues/1773)
diff --git a/docs/content/theming.md b/docs/content/theming.md
index 7fc0840b3bc..79faf1cfe8e 100644
--- a/docs/content/theming.md
+++ b/docs/content/theming.md
@@ -176,6 +176,16 @@ function Example() {
}
```
+#### `preventSSRMismatch` prop
+
+If you are doing server-side rendering, pass the `preventSSRMismatch` prop to ensure the rendered output from the server and browser match even when they resolve "auto" color mode differently.
+
+```jsx
+
+ ...
+
+```
+
### Setting color schemes
To choose which color schemes will be displayed in `day` and `night` mode, use the `dayScheme` and `nightScheme` props on `ThemeProvider` or the `setDayScheme` and `setNightScheme` functions from the `useTheme` hook:
diff --git a/src/ThemeProvider.tsx b/src/ThemeProvider.tsx
index 1c314ff4052..b1bd76846e7 100644
--- a/src/ThemeProvider.tsx
+++ b/src/ThemeProvider.tsx
@@ -17,6 +17,7 @@ export type ThemeProviderProps = {
colorMode?: ColorModeWithAuto
dayScheme?: string
nightScheme?: string
+ preventSSRMismatch?: boolean
}
const ThemeContext = React.createContext<{
@@ -47,21 +48,50 @@ export const ThemeProvider: React.FC = ({children, ...props}
// Initialize state
const theme = props.theme ?? fallbackTheme ?? defaultTheme
+
+ const resolvedColorModePassthrough = React.useRef(
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore This custom variable does not exist on window because we set it outselves
+ typeof window !== 'undefined' ? window.__PRIMER_RESOLVED_SERVER_COLOR_MODE : undefined
+ )
+
const [colorMode, setColorMode] = React.useState(props.colorMode ?? fallbackColorMode ?? defaultColorMode)
const [dayScheme, setDayScheme] = React.useState(props.dayScheme ?? fallbackDayScheme ?? defaultDayScheme)
const [nightScheme, setNightScheme] = React.useState(props.nightScheme ?? fallbackNightScheme ?? defaultNightScheme)
const systemColorMode = useSystemColorMode()
- const resolvedColorMode = resolveColorMode(colorMode, systemColorMode)
+ const resolvedColorMode = resolvedColorModePassthrough.current || resolveColorMode(colorMode, systemColorMode)
const colorScheme = chooseColorScheme(resolvedColorMode, dayScheme, nightScheme)
const {resolvedTheme, resolvedColorScheme} = React.useMemo(
() => applyColorScheme(theme, colorScheme),
[theme, colorScheme]
)
+ // this effect will only run on client
+ React.useEffect(
+ function updateColorModeAfterServerPassthorugh() {
+ const resolvedColorModeOnClient = resolveColorMode(colorMode, systemColorMode)
+
+ if (resolvedColorModePassthrough.current) {
+ // if the resolved color mode passed on from the server is not the resolved color mode on client, change it!
+ if (resolvedColorModePassthrough.current !== resolvedColorModeOnClient) {
+ window.setTimeout(() => {
+ // override colorMode to whatever is resolved on the client to get a re-render
+ setColorMode(resolvedColorModeOnClient)
+ // immediately after that, set the colorMode to what the user passed to respond to system color mode changes
+ setColorMode(colorMode)
+ })
+ }
+
+ resolvedColorModePassthrough.current = null
+ }
+ },
+ [colorMode, systemColorMode]
+ )
+
// Update state if props change
React.useEffect(() => {
setColorMode(props.colorMode ?? fallbackColorMode ?? defaultColorMode)
- }, [props.colorMode, fallbackColorMode])
+ }, [props.colorMode, resolvedColorMode, fallbackColorMode])
React.useEffect(() => {
setDayScheme(props.dayScheme ?? fallbackDayScheme ?? defaultDayScheme)
@@ -86,7 +116,12 @@ export const ThemeProvider: React.FC = ({children, ...props}
setNightScheme
}}
>
- {children}
+
+ {children}
+ {props.preventSSRMismatch ? (
+
+ ) : null}
+
)
}