-
-
Notifications
You must be signed in to change notification settings - Fork 8.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(theme): allow reverting to auto theme mode #8474
base: main
Are you sure you want to change the base?
Conversation
I'm getting errors like:
I don't really understand why nor how I can debug this. Any hint? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks
It doesn't build so it requires at least a few changes 🤪
Will do a better review of the rest when I have time
return ColorModes.dark; | ||
case ColorModeChoices.auto: | ||
default: | ||
return window.matchMedia('(prefers-color-scheme: dark)').matches |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's not possible to access window when rendering React components (and useMemo is executed when rendering).
When doing SSR/SSG, window does not exist and it's impossible to query for prefers color scheme because we are not inside a browser.
The error maybe doesn't explain this well, but with experience we figure out the problem quite fast:
6:29:39 AM: [ERROR] Docusaurus server-side rendering could not render static page with path /docs/2.1.0/creating-pages/.
6:29:39 AM: [INFO] It looks like you are using code that should run on the client-side only.
6:29:39 AM: To get around it, try using `<BrowserOnly>` (https://docusaurus.io/docs/docusaurus-core/#browseronly) or `ExecutionEnvironment` (https://docusaurus.io/docs/docusaurus-core/#executionenvironment).
6:29:39 AM: It might also require to wrap your client code in `useEffect` hook and/or import a third-party library dynamically (if any).
6:30:15 AM: ReferenceError: window is not defined
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oooh that makes a lot of sense yeah
so what's the plan here, since we cannot know in advance what theme to render with from the server-side? do i just assume light when window
isn't available or is there something smarter to do?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nasso We use defaultTheme
, which always statically exists.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the thing is, defaultMode
now includes auto
, but here we need to figure out the effective theme (light
or dark
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not possible to figure out the effective theme during the SSR render phase and unfortunately will never be possible. There's no theme when running in Node.js. That's why we use inlined JS and the data-theme attribute to set the effective theme as soon as we can to avoid a "theme flash". Doing this in render/useMemo is both too late and not possible. Doing this in useEffect is possible but also too late to avoid the flash.
I think there's already something we do in existing code that is even not compatible with React 18. The server render must match exactly the first client render (hydration). And we can't get the effective theme on the server, so both the server/first-client render should use the exact same value (the default theme, not the effective one).
I mean: this existing code is likely wrong because it can lead to different values for the server render and the first client render:
const getInitialColorMode = (
defaultMode: ColorModeChoice | undefined,
): ColorModeChoice =>
coerceToColorMode(
ExecutionEnvironment.canUseDOM
? document.documentElement.getAttribute('data-theme-choice') ??
document.documentElement.getAttribute('data-theme')
: defaultMode,
);
It should simply be instead: useState(defaultMode)
The effective theme state can only be set later, after hydration, at best in a useLayoutEffect
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think one way we could get rid of the FOUC altogether would be to handle auto
as a possible value for the data-theme
attribute, and use @media
queries in CSS to set the theme instead of dynamically querying the preference using JS. CSS media queries would only apply on [data-theme=auto]
. this would probably require modifying a lot of CSS though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a possibility, but I think it will duplicate CSS, and also be annoying for users that already have custom dark/light mode elements.
We/they would now have to use more complex selectors everywhere:
@media (prefers-color-scheme: dark) [data-theme=auto] .subselector, [data-theme=dark] .subselector { ... }
@media (prefers-color-scheme: light) [data-theme=auto] .subselector, [data-theme=light] .subselector { ... }
So I don't think it would be a great move, our current theming CSS DX is better:
[data-theme=dark] .subselector { ... }
[data-theme=light] .subselector { ... }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that is definitely much better.
And we can't get the effective theme on the server, so both the server/first-client render should use the exact same value (the default theme, not the effective one).
does that mean there would be no way to set the default theme to the system preference? because i don't see what the server would render when the default is "auto"
✅ [V2]Built without sensitive environment variables
To edit notification comments on pull requests, go to your Netlify site settings. |
⚡️ Lighthouse report for the deploy preview of this PR
|
woops didnt know github would remove the other review requests 🤔 |
Pre-flight checklist
Motivation
See #8074.
When the user changes the theme manually by clicking on the toggle, the user's preference gets persisted in
localStorage
, and there's no way to go back to the system/OS/browser theme (which I just call "auto"; browser is free to report anything it wants, not necessarily what it or the OS is using). I added a third state to the existing toggle, that allows the user to set the theme to "automatic" mode, which will honourprefers-color-scheme
. The preference of the user is still persisted tolocalStorage
, except now we sometimes storetheme=auto
. I created a rudimentary icon in Figma, inspired by MDN's icon but in the style of the rest of the icons in Docusaurus' classic theme (i.e. stroke width, margins).As described in #8074 (comment), a
colorModeChoice
was introduced to support the third value:auto
.colorMode
is now computed fromcolorModeChoice
, the source of truth. I also followed the same logic for the state machine cycling logic.On the DOM side of things (what actually changes the theme), the
data-theme
attribute still contains eitherdark
orlight
(I thought about allowing it to beauto
and using CSS media queries but that's too much work right now, so probably out of scope). To support the third state in the theme toggle button, I added adata-theme-choice
attribute, that may also beauto
. The logic otherwise stays the same.Finally, the current configuration options include
respectPrefersColorScheme
, which is used to determine whetherlight
ordark
should be used when the user has not manually changed the theme. I deprecatedrespectPrefersColorScheme
in favour of a third possible value fordefaultMode
:auto
. This makes the logic much easier to follow:light
will always be light,dark
will always be dark, andauto
will always use whatever is the preferred colour scheme.Test Plan
I think the only way to test it would be with E2E tests? I did not add any because I couldn't find any test for the previous behaviour (i.e. clicking the toggle changes the
data-theme
attribute).prefers-color-scheme: dark
prefers-color-scheme: light
Test links
Deploy preview: https://deploy-preview-8474--docusaurus-2.netlify.app/
Related issues/PRs
Fixes #8074.