Skip to content
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

Fix Light theme flash during page load #8004

Merged
merged 2 commits into from
Oct 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 0 additions & 85 deletions docs/components/DarkModeBtn.tsx

This file was deleted.

4 changes: 2 additions & 2 deletions docs/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { Wrapper } from './primitives/Wrapper';
import { Hamburger } from './icons/Hamburger';
import { Button } from './primitives/Button';
import { NavItem } from './docs/Navigation';
import { DarkModeBtn } from './DarkModeBtn';
import { ThemeToggle } from './ThemeToggle';
import { Keystone } from './icons/Keystone';
import { MobileMenu } from './MobileMenu';
import { GitHub } from './icons/GitHub';
Expand Down Expand Up @@ -293,7 +293,7 @@ export function Header() {
>
Documentation
</Button>
<DarkModeBtn />
<ThemeToggle />
<a
href="https://github.com/keystonejs/keystone"
target="_blank"
Expand Down
21 changes: 2 additions & 19 deletions docs/components/Theme.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,16 @@
/** @jsxRuntime classic */
/** @jsx jsx */
import { jsx, Global } from '@emotion/react';
import { useState, useEffect } from 'react';

import { COLORS, SPACE, TYPE, TYPESCALE } from '../lib/TOKENS';

export function Theme() {
const [theme, setTheme] = useState<keyof typeof COLORS>('light');

useEffect(() => {
// we duplicate the logic of DarkModeBtn here so the flash is shorter
const detectedTheme =
(localStorage.getItem('theme') ||
window.matchMedia('(prefers-color-scheme: dark)').matches ||
'light') === 'dark'
? 'dark'
: 'light';
localStorage.setItem('theme', detectedTheme);

if (detectedTheme !== 'light') {
setTheme(detectedTheme);
}
}, []);

return (
<Global
styles={{
'[data-theme="light"]': { ...COLORS['light'] },
'[data-theme="dark"]': { ...COLORS['dark'] },
':root': {
...COLORS[theme],
...SPACE,
...TYPE,
...TYPESCALE,
Expand Down
71 changes: 71 additions & 0 deletions docs/components/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/** @jsxRuntime classic */
/** @jsx jsx */
import { Fragment, useState, useEffect, HTMLAttributes } from 'react';
import { jsx } from '@emotion/react';

import { COLORS } from '../lib/TOKENS';
import { LightMode } from './icons/LightMode';
import { DarkMode } from './icons/DarkMode';

function ModeIcon({ theme }: { theme: 'light' | 'dark' }) {
if (theme === 'dark') {
return <LightMode css={{ height: 'var(--space-xlarge)' }} />;
}

return <DarkMode css={{ height: 'var(--space-xlarge)' }} />;
}

export function ThemeToggle(props: HTMLAttributes<HTMLButtonElement>) {
/*
We don't want to render the toggle during server rendering
because Next will always server render the light mode toggle and hydrate the light mode toggle
even if the theme is dark mode based on system preference.
So we render the toggle only on the client
*/
const [theme, setTheme] = useState<keyof typeof COLORS | null>(null);

useEffect(() => {
const currentTheme = document.documentElement.getAttribute('data-theme') as 'light' | 'dark';
setTheme(currentTheme);
}, [setTheme]);

const handleThemeChange = () => {
const newTheme = theme === 'dark' ? 'light' : 'dark';
if (newTheme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
} else {
document.documentElement.setAttribute('data-theme', 'light');
}
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
};

return (
<Fragment>
<button
key={theme}
onClick={handleThemeChange}
css={{
display: 'inline-flex',
appearance: 'none',
background: 'transparent',
boxShadow: 'none',
border: '0 none',
borderRadius: '100%',
lineHeight: 1,
padding: 0,
margin: 0,
color: 'var(--muted)',
cursor: 'pointer',
transition: 'color 0.3s ease',
'&:hover, &:focus': {
color: 'var(--link)',
},
}}
{...props}
>
{theme === null ? <span css={{ width: 24 }} /> : <ModeIcon theme={theme} />}
</button>
</Fragment>
);
}
31 changes: 30 additions & 1 deletion docs/pages/_document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class MyDocument extends Document {

render() {
return (
<Html>
<Html data-theme="light">
<Head>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
Expand All @@ -54,6 +54,35 @@ class MyDocument extends Document {
src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js"
/>
<script data-no-cookie data-respect-dnt async data-api="/_sb" src="/sb.js" />
{/*
The page is server rendered and hydrated on the browser client.
While server rendering we do not know what the user's preferred/saved theme is and default to light theme.
So we run this script in the browser before the page is rendered (not the react render but the browser render)
and set the theme class in html element so our styles would know which theme to paint on first paint.
All this to avoid a flash of default light theme when user either prefers dark theme or has previously
saved dark theme to their local storage. ¯\_(ツ)_/¯ React is hard sometimes.
*/}
<script
dangerouslySetInnerHTML={{
__html: `
(function () {
if (typeof window !== 'undefined') {
const isSystemColorSchemeDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const localStorageTheme = localStorage.theme;
if (!localStorageTheme && isSystemColorSchemeDark) {
document.documentElement.setAttribute('data-theme', 'dark');
} else if (localStorageTheme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
} else {
// we already server render light theme
// so this is just ensuring that
document.documentElement.setAttribute('data-theme', 'light');
}
}
})();
`,
}}
/>
</Head>
<body>
<Main />
Expand Down