feat(theme): Allow resetting colorMode to System/OS value (#10987)

* make it work

* fix

* Try to fix accessibility issues

* add translations

* rename 'auto' to 'system'

* refactor: apply lint autofix

* rename 'auto' to 'system'

* remove title prop

* typo

* use shorter title

* refactor: apply lint autofix

* document useColorMode tradeoffs + data-attribute variables

---------

Co-authored-by: slorber <749374+slorber@users.noreply.github.com>
Co-authored-by: nasso
Co-authored-by: OzakIOne
This commit is contained in:
Sébastien Lorber 2025-03-14 13:45:25 +01:00 committed by GitHub
parent fd51384cab
commit 7cf94c03a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 394 additions and 146 deletions

View file

@ -30,43 +30,22 @@ export function getThemeInlineScript({
return `(function() {
var defaultMode = '${defaultMode}';
var respectPrefersColorScheme = ${respectPrefersColorScheme};
function setDataThemeAttribute(theme) {
document.documentElement.setAttribute('data-theme', theme);
function getSystemColorMode() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function getQueryStringTheme() {
try {
return new URLSearchParams(window.location.search).get('${ThemeQueryStringKey}')
} catch (e) {
} catch (e) {}
}
}
function getStoredTheme() {
try {
return window['${siteStorage.type}'].getItem('${themeStorageKey}');
} catch (err) {
} catch (err) {}
}
}
var initialTheme = getQueryStringTheme() || getStoredTheme();
if (initialTheme !== null) {
setDataThemeAttribute(initialTheme);
} else {
if (
respectPrefersColorScheme &&
window.matchMedia('(prefers-color-scheme: dark)').matches
) {
setDataThemeAttribute('dark');
} else if (
respectPrefersColorScheme &&
window.matchMedia('(prefers-color-scheme: light)').matches
) {
setDataThemeAttribute('light');
} else {
setDataThemeAttribute(defaultMode === 'dark' ? 'dark' : 'light');
}
}
document.documentElement.setAttribute('data-theme', initialTheme || (respectPrefersColorScheme ? getSystemColorMode() : defaultMode));
document.documentElement.setAttribute('data-theme-choice', initialTheme || (respectPrefersColorScheme ? 'system' : defaultMode));
})();`;
}

View file

@ -1555,12 +1555,13 @@ declare module '@theme/ColorModeToggle' {
export interface Props {
readonly className?: string;
readonly buttonClassName?: string;
readonly value: ColorMode;
readonly respectPrefersColorScheme: boolean;
readonly value: ColorMode | null;
/**
* The parameter represents the "to-be" value. For example, if currently in
* dark mode, clicking the button should call `onChange("light")`
* light mode, clicking the button should call `onChange("dark")`
*/
readonly onChange: (colorMode: ColorMode) => void;
readonly onChange: (colorMode: ColorMode | null) => void;
}
export default function ColorModeToggle(props: Props): ReactNode;
@ -1617,6 +1618,14 @@ declare module '@theme/Icon/LightMode' {
export default function IconLightMode(props: Props): ReactNode;
}
declare module '@theme/Icon/SystemColorMode' {
import type {ComponentProps} from 'react';
export interface Props extends ComponentProps<'svg'> {}
export default function IconSystemColorMode(props: Props): JSX.Element;
}
declare module '@theme/Icon/Menu' {
import type {ComponentProps, ReactNode} from 'react';

View file

@ -11,40 +11,105 @@ import useIsBrowser from '@docusaurus/useIsBrowser';
import {translate} from '@docusaurus/Translate';
import IconLightMode from '@theme/Icon/LightMode';
import IconDarkMode from '@theme/Icon/DarkMode';
import IconSystemColorMode from '@theme/Icon/SystemColorMode';
import type {Props} from '@theme/ColorModeToggle';
import type {ColorMode} from '@docusaurus/theme-common';
import styles from './styles.module.css';
// The order of color modes is defined here, and can be customized with swizzle
function getNextColorMode(
colorMode: ColorMode | null,
respectPrefersColorScheme: boolean,
) {
// 2-value transition
if (!respectPrefersColorScheme) {
return colorMode === 'dark' ? 'light' : 'dark';
}
// 3-value transition
switch (colorMode) {
case null:
return 'light';
case 'light':
return 'dark';
case 'dark':
return null;
default:
throw new Error(`unexpected color mode ${colorMode}`);
}
}
function getColorModeLabel(colorMode: ColorMode | null): string {
switch (colorMode) {
case null:
return translate({
message: 'system mode',
id: 'theme.colorToggle.ariaLabel.mode.system',
description: 'The name for the system color mode',
});
case 'light':
return translate({
message: 'light mode',
id: 'theme.colorToggle.ariaLabel.mode.light',
description: 'The name for the light color mode',
});
case 'dark':
return translate({
message: 'dark mode',
id: 'theme.colorToggle.ariaLabel.mode.dark',
description: 'The name for the dark color mode',
});
default:
throw new Error(`unexpected color mode ${colorMode}`);
}
}
function getColorModeAriaLabel(colorMode: ColorMode | null) {
return translate(
{
message: 'Switch between dark and light mode (currently {mode})',
id: 'theme.colorToggle.ariaLabel',
description: 'The ARIA label for the color mode toggle',
},
{
mode: getColorModeLabel(colorMode),
},
);
}
function CurrentColorModeIcon(): ReactNode {
// 3 icons are always rendered for technical reasons
// We use "data-theme-choice" to render the correct one
// This must work even before React hydrates
return (
<>
<IconLightMode
// a18y is handled at the button level,
// not relying on button content (svg icons)
aria-hidden
className={clsx(styles.toggleIcon, styles.lightToggleIcon)}
/>
<IconDarkMode
aria-hidden
className={clsx(styles.toggleIcon, styles.darkToggleIcon)}
/>
<IconSystemColorMode
aria-hidden
className={clsx(styles.toggleIcon, styles.systemToggleIcon)}
/>
</>
);
}
function ColorModeToggle({
className,
buttonClassName,
respectPrefersColorScheme,
value,
onChange,
}: Props): ReactNode {
const isBrowser = useIsBrowser();
const title = translate(
{
message: 'Switch between dark and light mode (currently {mode})',
id: 'theme.colorToggle.ariaLabel',
description: 'The ARIA label for the navbar color mode toggle',
},
{
mode:
value === 'dark'
? translate({
message: 'dark mode',
id: 'theme.colorToggle.ariaLabel.mode.dark',
description: 'The name for the dark color mode',
})
: translate({
message: 'light mode',
id: 'theme.colorToggle.ariaLabel.mode.light',
description: 'The name for the light color mode',
}),
},
);
return (
<div className={clsx(styles.toggle, className)}>
<button
@ -55,18 +120,23 @@ function ColorModeToggle({
buttonClassName,
)}
type="button"
onClick={() => onChange(value === 'dark' ? 'light' : 'dark')}
onClick={() =>
onChange(getNextColorMode(value, respectPrefersColorScheme))
}
disabled={!isBrowser}
title={title}
aria-label={title}
aria-live="polite"
aria-pressed={value === 'dark' ? 'true' : 'false'}>
<IconLightMode
className={clsx(styles.toggleIcon, styles.lightToggleIcon)}
/>
<IconDarkMode
className={clsx(styles.toggleIcon, styles.darkToggleIcon)}
/>
title={getColorModeLabel(value)}
aria-label={getColorModeAriaLabel(value)}
// For accessibility decisions
// See https://github.com/facebook/docusaurus/issues/7667#issuecomment-2724401796
// aria-live disabled on purpose - This is annoying because:
// - without this attribute, VoiceOver doesn't announce on button enter
// - with this attribute, VoiceOver announces twice on ctrl+opt+space
// - with this attribute, NVDA announces many times
// aria-live="polite"
>
<CurrentColorModeIcon />
</button>
</div>
);

View file

@ -25,11 +25,16 @@
background: var(--ifm-color-emphasis-200);
}
[data-theme='light'] .darkToggleIcon,
[data-theme='dark'] .lightToggleIcon {
.toggleIcon {
display: none;
}
[data-theme-choice='system'] .systemToggleIcon,
[data-theme-choice='light'] .lightToggleIcon,
[data-theme-choice='dark'] .darkToggleIcon {
display: initial;
}
.toggleButtonDisabled {
cursor: not-allowed;
}

View file

@ -0,0 +1,20 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type {ReactNode} from 'react';
import type {Props} from '@theme/Icon/SystemColorMode';
export default function IconSystemColorMode(props: Props): ReactNode {
return (
<svg viewBox="0 0 24 24" width={24} height={24} {...props}>
<path
fill="currentColor"
d="m12 21c4.971 0 9-4.029 9-9s-4.029-9-9-9-9 4.029-9 9 4.029 9 9 9zm4.95-13.95c1.313 1.313 2.05 3.093 2.05 4.95s-0.738 3.637-2.05 4.95c-1.313 1.313-3.093 2.05-4.95 2.05v-14c1.857 0 3.637 0.737 4.95 2.05z"
/>
</svg>
);
}

View file

@ -13,10 +13,10 @@ import styles from './styles.module.css';
export default function NavbarColorModeToggle({className}: Props): ReactNode {
const navbarStyle = useThemeConfig().navbar.style;
const disabled = useThemeConfig().colorMode.disableSwitch;
const {colorMode, setColorMode} = useColorMode();
const {disableSwitch, respectPrefersColorScheme} = useThemeConfig().colorMode;
const {colorModeChoice, setColorMode} = useColorMode();
if (disabled) {
if (disableSwitch) {
return null;
}
@ -26,7 +26,8 @@ export default function NavbarColorModeToggle({className}: Props): ReactNode {
buttonClassName={
navbarStyle === 'dark' ? styles.darkNavbarColorModeToggle : undefined
}
value={colorMode}
respectPrefersColorScheme={respectPrefersColorScheme}
value={colorModeChoice}
onChange={setColorMode}
/>
);

View file

@ -11,19 +11,49 @@ import React, {
useEffect,
useContext,
useMemo,
useRef,
type ReactNode,
} from 'react';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import {ReactContextError} from '../utils/reactUtils';
import {createStorageSlot} from '../utils/storageUtils';
import {useThemeConfig} from '../utils/useThemeConfig';
// The "effective" color mode
export type ColorMode = 'light' | 'dark';
// The color mode explicitly chosen by the user
// null => no choice has been made, or the choice has been reverted to OS value
export type ColorModeChoice = ColorMode | null;
function getSystemColorMode(): ColorMode {
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}
function subscribeToMedia(
query: string,
listener: (event: MediaQueryListEvent) => void,
): () => void {
const mql = window.matchMedia(query);
mql.addEventListener('change', listener);
return () => mql.removeEventListener('change', listener);
}
function subscribeToSystemColorModeChange(
onChange: (newSystemColorMode: ColorMode) => void,
): () => void {
return subscribeToMedia('(prefers-color-scheme: dark)', () =>
onChange(getSystemColorMode()),
);
}
type ContextValue = {
/** Current color mode. */
/** The effective color mode. */
readonly colorMode: ColorMode;
/** The explicitly chosen color mode */
readonly colorModeChoice: ColorModeChoice;
/** Set new color mode. */
readonly setColorMode: (colorMode: ColorMode) => void;
readonly setColorMode: (colorMode: ColorModeChoice) => void;
// TODO Docusaurus v4
// legacy APIs kept for retro-compatibility: deprecate them
@ -37,16 +67,17 @@ const Context = React.createContext<ContextValue | undefined>(undefined);
const ColorModeStorageKey = 'theme';
const ColorModeStorage = createStorageSlot(ColorModeStorageKey);
const ColorModes = {
light: 'light',
dark: 'dark',
} as const;
export type ColorMode = (typeof ColorModes)[keyof typeof ColorModes];
// We use data-theme-choice="system", not an absent attribute
// This is easier to handle for users with CSS
const SystemAttribute = 'system';
// Ensure to always return a valid colorMode even if input is invalid
const coerceToColorMode = (colorMode?: string | null): ColorMode =>
colorMode === ColorModes.dark ? ColorModes.dark : ColorModes.light;
const coerceToColorMode = (colorMode: string | null): ColorMode =>
colorMode === 'dark' ? 'dark' : 'light';
const coerceToColorModeChoice = (colorMode: string | null): ColorModeChoice =>
colorMode === null || colorMode === SystemAttribute
? null
: coerceToColorMode(colorMode);
const ColorModeAttribute = {
get: () => {
@ -62,15 +93,26 @@ const ColorModeAttribute = {
},
};
const readInitialColorMode = (): ColorMode => {
if (!ExecutionEnvironment.canUseDOM) {
throw new Error("Can't read initial color mode on the server");
}
return ColorModeAttribute.get();
const ColorModeChoiceAttribute = {
get: () => {
return coerceToColorModeChoice(
document.documentElement.getAttribute('data-theme-choice'),
);
},
set: (colorMode: ColorModeChoice) => {
document.documentElement.setAttribute(
'data-theme-choice',
coerceToColorModeChoice(colorMode) ?? SystemAttribute,
);
},
};
const storeColorMode = (newColorMode: ColorMode) => {
const persistColorModeChoice = (newColorMode: ColorModeChoice) => {
if (newColorMode === null) {
ColorModeStorage.del();
} else {
ColorModeStorage.set(coerceToColorMode(newColorMode));
}
};
// The color mode state is initialized in useEffect on purpose
@ -83,20 +125,33 @@ function useColorModeState() {
colorMode: {defaultMode},
} = useThemeConfig();
const [colorMode, setColorModeState] = useState(defaultMode);
const [colorMode, setColorModeState] = useState<ColorMode>(defaultMode);
const [colorModeChoice, setColorModeChoiceState] =
useState<ColorModeChoice>(null);
useEffect(() => {
setColorModeState(readInitialColorMode());
setColorModeState(ColorModeAttribute.get());
setColorModeChoiceState(ColorModeChoiceAttribute.get());
}, []);
return [colorMode, setColorModeState] as const;
return {
colorMode,
setColorModeState,
colorModeChoice,
setColorModeChoiceState,
} as const;
}
function useContextValue(): ContextValue {
const {
colorMode: {defaultMode, disableSwitch, respectPrefersColorScheme},
} = useThemeConfig();
const [colorMode, setColorModeState] = useColorModeState();
const {
colorMode,
setColorModeState,
colorModeChoice,
setColorModeChoiceState,
} = useColorModeState();
useEffect(() => {
// A site is deployed without disableSwitch
@ -109,67 +164,70 @@ function useContextValue(): ContextValue {
}, [disableSwitch]);
const setColorMode = useCallback(
(newColorMode: ColorMode | null, options: {persist?: boolean} = {}) => {
(
newColorModeChoice: ColorModeChoice,
options: {persist?: boolean} = {},
) => {
const {persist = true} = options;
if (newColorMode) {
// Reset to system/default color mode
if (newColorModeChoice === null) {
// Set the effective color
const newColorMode = respectPrefersColorScheme
? getSystemColorMode()
: defaultMode;
ColorModeAttribute.set(newColorMode);
setColorModeState(newColorMode);
// Set the chosen color
ColorModeChoiceAttribute.set(null);
setColorModeChoiceState(null);
}
// Happy case, when an explicit color is provided
else {
ColorModeAttribute.set(newColorModeChoice);
ColorModeChoiceAttribute.set(newColorModeChoice);
setColorModeState(newColorModeChoice);
setColorModeChoiceState(newColorModeChoice);
}
if (persist) {
storeColorMode(newColorMode);
}
} else {
if (respectPrefersColorScheme) {
const osColorMode = window.matchMedia('(prefers-color-scheme: dark)')
.matches
? ColorModes.dark
: ColorModes.light;
ColorModeAttribute.set(osColorMode);
setColorModeState(osColorMode);
} else {
ColorModeAttribute.set(defaultMode);
setColorModeState(defaultMode);
}
ColorModeStorage.del();
persistColorModeChoice(newColorModeChoice);
}
},
[setColorModeState, respectPrefersColorScheme, defaultMode],
[
setColorModeState,
setColorModeChoiceState,
respectPrefersColorScheme,
defaultMode,
],
);
// Synchronize theme color/choice mode with browser storage
useEffect(() => {
if (disableSwitch) {
return undefined;
}
return ColorModeStorage.listen((e) => {
setColorMode(coerceToColorMode(e.newValue));
setColorMode(coerceToColorModeChoice(e.newValue));
});
}, [disableSwitch, setColorMode]);
// PCS is coerced to light mode when printing, which causes the color mode to
// be reset to dark when exiting print mode, disregarding user settings. When
// the listener fires only because of a print/screen switch, we don't change
// color mode. See https://github.com/facebook/docusaurus/pull/6490
const previousMediaIsPrint = useRef(false);
}, [setColorMode]);
// Synchronize theme color with system color
useEffect(() => {
if (disableSwitch && !respectPrefersColorScheme) {
if (colorModeChoice !== null || !respectPrefersColorScheme) {
return undefined;
}
const mql = window.matchMedia('(prefers-color-scheme: dark)');
const onChange = () => {
if (window.matchMedia('print').matches || previousMediaIsPrint.current) {
previousMediaIsPrint.current = window.matchMedia('print').matches;
return;
}
setColorMode(null);
};
mql.addListener(onChange);
return () => mql.removeListener(onChange);
}, [setColorMode, disableSwitch, respectPrefersColorScheme]);
return subscribeToSystemColorModeChange((newSystemColorMode) => {
// Note: we don't use "setColorMode" on purpose
// The system changes should never be considered an explicit theme choice
// They only affect the "effective" color, and should never be persisted
// Note: this listener also fire when printing, see https://github.com/facebook/docusaurus/pull/6490
setColorModeState(newSystemColorMode);
ColorModeAttribute.set(newSystemColorMode);
});
}, [respectPrefersColorScheme, colorModeChoice, setColorModeState]);
return useMemo(
() => ({
colorMode,
colorModeChoice,
setColorMode,
get isDarkTheme() {
if (process.env.NODE_ENV === 'development') {
@ -177,7 +235,7 @@ function useContextValue(): ContextValue {
'`useColorMode().isDarkTheme` is deprecated. Please use `useColorMode().colorMode === "dark"` instead.',
);
}
return colorMode === ColorModes.dark;
return colorMode === 'dark';
},
setLightTheme() {
if (process.env.NODE_ENV === 'development') {
@ -185,7 +243,7 @@ function useContextValue(): ContextValue {
'`useColorMode().setLightTheme` is deprecated. Please use `useColorMode().setColorMode("light")` instead.',
);
}
setColorMode(ColorModes.light);
setColorMode('light');
},
setDarkTheme() {
if (process.env.NODE_ENV === 'development') {
@ -193,10 +251,10 @@ function useContextValue(): ContextValue {
'`useColorMode().setDarkTheme` is deprecated. Please use `useColorMode().setColorMode("dark")` instead.',
);
}
setColorMode(ColorModes.dark);
setColorMode('dark');
},
}),
[colorMode, setColorMode],
[colorMode, colorModeChoice, setColorMode],
);
}

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "التبديل بين الوضع الداكن والفاتح (الحالي {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "الوضع الداكن",
"theme.colorToggle.ariaLabel.mode.light": "الوضع الفاتح",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "تعديل هذه الصفحة",
"theme.common.headingLinkTitle": "ارتباط مباشر بالعنوان {heading}",
"theme.common.skipToMainContent": "انتقل إلى المحتوى الرئيسي",

View file

@ -78,11 +78,13 @@
"theme.blog.tagTitle": "{nPosts} tagged with \"{tagName}\"",
"theme.blog.tagTitle___DESCRIPTION": "The title of the page for a blog tag",
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
"theme.colorToggle.ariaLabel___DESCRIPTION": "The ARIA label for the navbar color mode toggle",
"theme.colorToggle.ariaLabel___DESCRIPTION": "The ARIA label for the color mode toggle",
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
"theme.colorToggle.ariaLabel.mode.dark___DESCRIPTION": "The name for the dark color mode",
"theme.colorToggle.ariaLabel.mode.light": "light mode",
"theme.colorToggle.ariaLabel.mode.light___DESCRIPTION": "The name for the light color mode",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.colorToggle.ariaLabel.mode.system___DESCRIPTION": "The name for the system color mode",
"theme.common.editThisPage": "Edit this page",
"theme.common.editThisPage___DESCRIPTION": "The link label to edit the current page",
"theme.common.headingLinkTitle": "Direct link to {heading}",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Превключване между тъмен и светъл режим (В момента {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "тъмен режим",
"theme.colorToggle.ariaLabel.mode.light": "светъл режим",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "Редактирай тази страница",
"theme.common.headingLinkTitle": "Директна връзка към {heading}",
"theme.common.skipToMainContent": "Преминете към основното съдържание",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
"theme.colorToggle.ariaLabel.mode.light": "light mode",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "এই পেজটি এডিট করুন",
"theme.common.headingLinkTitle": "{heading} এর সঙ্গে সরাসরি লিংকড",
"theme.common.skipToMainContent": "স্কিপ করে মূল কন্টেন্ট এ যান",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
"theme.colorToggle.ariaLabel.mode.light": "light mode",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "Upravit tuto stránku",
"theme.common.headingLinkTitle": "Přímý odkaz na {heading}",
"theme.common.skipToMainContent": "Přeskočit na hlavní obsah",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
"theme.colorToggle.ariaLabel.mode.light": "light mode",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "Rediger denne side",
"theme.common.headingLinkTitle": "Direkte link til {heading}",
"theme.common.skipToMainContent": "Hop til hovedindhold",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Umschalten zwischen dunkler und heller Ansicht (momentan {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "dunkler Modus",
"theme.colorToggle.ariaLabel.mode.light": "heller Modus",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "Diese Seite bearbeiten",
"theme.common.headingLinkTitle": "Direkter Link zur {heading}",
"theme.common.skipToMainContent": "Zum Hauptinhalt springen",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Cambiar entre modo oscuro y claro (actualmente {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "modo oscuro",
"theme.colorToggle.ariaLabel.mode.light": "modo claro",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "Editar esta página",
"theme.common.headingLinkTitle": "Enlace directo al {heading}",
"theme.common.skipToMainContent": "Saltar al contenido principal",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Vaheta heleda ja tumeda teema vahel (currently {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "tume teema",
"theme.colorToggle.ariaLabel.mode.light": "hele teema",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "Redigeeri seda lehte",
"theme.common.headingLinkTitle": "Link {heading}",
"theme.common.skipToMainContent": "Liigu peamise sisu juurde",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "بین حالت تاریک و روشن سوئیچ کنید (الان {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "حالت تیره",
"theme.colorToggle.ariaLabel.mode.light": "حالت روشن",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "ویرایش مطالب این صفحه",
"theme.common.headingLinkTitle": "لینک مستقیم به {heading}",
"theme.common.skipToMainContent": "پرش به مطلب اصلی",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
"theme.colorToggle.ariaLabel.mode.light": "light mode",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "I-edit ang page",
"theme.common.headingLinkTitle": "Direktang link patungo sa {heading}",
"theme.common.skipToMainContent": "Lumaktaw patungo sa pangunahing content",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Basculer entre le mode sombre et clair (actuellement {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "mode sombre",
"theme.colorToggle.ariaLabel.mode.light": "mode clair",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "Éditer cette page",
"theme.common.headingLinkTitle": "Lien direct vers {heading}",
"theme.common.skipToMainContent": "Aller au contenu principal",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
"theme.colorToggle.ariaLabel.mode.light": "light mode",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "ערוך דף זה",
"theme.common.headingLinkTitle": "קישור ישיר אל {heading}",
"theme.common.skipToMainContent": "דלג לתוכן הראשי",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
"theme.colorToggle.ariaLabel.mode.light": "light mode",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "इस पेज को बदलें",
"theme.common.headingLinkTitle": "{heading} का सीधा लिंक",
"theme.common.skipToMainContent": "मुख्य कंटेंट तक स्किप करें",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Váltás a sötét és világos mód között (jelenleg {mode} van beállítva)",
"theme.colorToggle.ariaLabel.mode.dark": "Sötét mód",
"theme.colorToggle.ariaLabel.mode.light": "Világos mód",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "Szerkesztés GitHub-on",
"theme.common.headingLinkTitle": "Közvetlen hivatkozás erre: {heading}",
"theme.common.skipToMainContent": "Ugrás a fő tartalomhoz",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Ubah antara modus gelap dan modus terang (saat ini {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "modus gelap",
"theme.colorToggle.ariaLabel.mode.light": "modus terang",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "Sunting halaman ini",
"theme.common.headingLinkTitle": "Taut langsung ke {heading}",
"theme.common.skipToMainContent": "Lewati ke konten utama",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Skiptu á milli rökkur stillingar og bjartar stillingar (núna {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "rökkur stilling",
"theme.colorToggle.ariaLabel.mode.light": "björt stilling",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "Breyttu þessari síðu",
"theme.common.headingLinkTitle": "Beinn hlekkur að {heading}",
"theme.common.skipToMainContent": "Hoppa yfir á aðal efni",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Passa dalla modalità scura a quella chiara (currently {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "Modalità scura",
"theme.colorToggle.ariaLabel.mode.light": "modalità luce",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "Modifica questa pagina",
"theme.common.headingLinkTitle": "Link diretto a {heading}",
"theme.common.skipToMainContent": "Passa al contenuto principale",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "ダークモードを切り替える(現在は{mode})",
"theme.colorToggle.ariaLabel.mode.dark": "ダークモード",
"theme.colorToggle.ariaLabel.mode.light": "ライトモード",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "このページを編集",
"theme.common.headingLinkTitle": "{heading} への直接リンク",
"theme.common.skipToMainContent": "メインコンテンツまでスキップ",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "어두운 모드와 밝은 모드 전환하기 (현재 {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "어두운 모드",
"theme.colorToggle.ariaLabel.mode.light": "밝은 모드",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "페이지 편집",
"theme.common.headingLinkTitle": "{heading}에 대한 직접 링크",
"theme.common.skipToMainContent": "본문으로 건너뛰기",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Bytt mellom mørk og lys utseende (nå {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "mørk utseende",
"theme.colorToggle.ariaLabel.mode.light": "lys utseende",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "Rediger denne siden",
"theme.common.headingLinkTitle": "Direkte lenke til {heading}",
"theme.common.skipToMainContent": "Gå til hovedinnhold",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Schakel tussen donkere en lichte modus (momenteel {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "donkere modus",
"theme.colorToggle.ariaLabel.mode.light": "lichte modus",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "Bewerk deze pagina",
"theme.common.headingLinkTitle": "Direct link naar {heading}",
"theme.common.skipToMainContent": "Ga naar hoofdinhoud",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Przełącz pomiędzy ciemnym a jasnym motywem (aktualnie ustawiony {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "ciemny motyw",
"theme.colorToggle.ariaLabel.mode.light": "jasny motyw",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "Edytuj tę stronę",
"theme.common.headingLinkTitle": "Bezpośredni link do {heading}",
"theme.common.skipToMainContent": "Przejdź do głównej zawartości",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Alterar entre os modos claro e escuro (modo {mode} ativado)",
"theme.colorToggle.ariaLabel.mode.dark": "modo escuro",
"theme.colorToggle.ariaLabel.mode.light": "modo claro",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "Editar essa página",
"theme.common.headingLinkTitle": "Link direto para {heading}",
"theme.common.skipToMainContent": "Pular para o conteúdo principal",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
"theme.colorToggle.ariaLabel.mode.light": "light mode",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "Editar esta página",
"theme.common.headingLinkTitle": "Link direto para {heading}",
"theme.common.skipToMainContent": "Saltar para o conteúdo principal",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Переключение между темным и светлым режимом (сейчас используется {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "Тёмный режим",
"theme.colorToggle.ariaLabel.mode.light": "Светлый режим",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "Отредактировать эту страницу",
"theme.common.headingLinkTitle": "Прямая ссылка на {heading}",
"theme.common.skipToMainContent": "Перейти к основному содержимому",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Preklopi med temnim in svetlim načinom (trenutno {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "temni način",
"theme.colorToggle.ariaLabel.mode.light": "svetli način",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "Uredi to stran",
"theme.common.headingLinkTitle": "Direktna povezava na {heading}",
"theme.common.skipToMainContent": "Preskoči na vsebino",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Switch between dark and light mode (currently {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
"theme.colorToggle.ariaLabel.mode.light": "light mode",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "Уреди ову страницу",
"theme.common.headingLinkTitle": "Веза до {heading}",
"theme.common.skipToMainContent": "Пређи на главни садржај",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Växla mellan mörkt och ljust utseende (currently {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "mörkt utseende",
"theme.colorToggle.ariaLabel.mode.light": "ljust utseende",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "Redigera denna sida",
"theme.common.headingLinkTitle": "Direktlänk till {heading}",
"theme.common.skipToMainContent": "Hoppa till huvudinnehåll",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Gijeki ýa-da gündizki temany saýlamak (häzirki wagtda {mode} ulanylýar)",
"theme.colorToggle.ariaLabel.mode.dark": "Gijeki tema",
"theme.colorToggle.ariaLabel.mode.light": "Gündizki tema",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "Bu sahypany üýtgetmek",
"theme.common.headingLinkTitle": "{heading} sahypa göni geçiň",
"theme.common.skipToMainContent": "Esasy mazmuna geç",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Karanlık ve aydınlık mod arasında geçiş yapın (şu anda {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "Karanlık mod",
"theme.colorToggle.ariaLabel.mode.light": "Aydınlık mod",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "Bu sayfayı düzenle",
"theme.common.headingLinkTitle": "{heading} doğrudan bağlantı",
"theme.common.skipToMainContent": "Ana içeriğe geç",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Перемикання між темним та світлим режимом (зараз використовується {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "Темний режим",
"theme.colorToggle.ariaLabel.mode.light": "Світлий режим",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "Відредагувати цю сторінку",
"theme.common.headingLinkTitle": "Пряме посилання на {heading}",
"theme.common.skipToMainContent": "Перейти до основного вмісту",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "Chuyển đổi chế độ sáng và tối (hiện tại là {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "chế độ tối",
"theme.colorToggle.ariaLabel.mode.light": "chế độ sáng",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "Sửa trang này",
"theme.common.headingLinkTitle": "Đường dẫn trực tiếp đến {heading}",
"theme.common.skipToMainContent": "Chuyển tới nội dung chính",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "切换浅色/暗黑模式(当前为{mode}",
"theme.colorToggle.ariaLabel.mode.dark": "暗黑模式",
"theme.colorToggle.ariaLabel.mode.light": "浅色模式",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "编辑此页",
"theme.common.headingLinkTitle": "{heading}的直接链接",
"theme.common.skipToMainContent": "跳到主要内容",

View file

@ -41,6 +41,7 @@
"theme.colorToggle.ariaLabel": "切換淺色/深色模式(當前為{mode}",
"theme.colorToggle.ariaLabel.mode.dark": "深色模式",
"theme.colorToggle.ariaLabel.mode.light": "淺色模式",
"theme.colorToggle.ariaLabel.mode.system": "system mode",
"theme.common.editThisPage": "編輯此頁",
"theme.common.headingLinkTitle": "{heading}的直接連結",
"theme.common.skipToMainContent": "跳至主要内容",

View file

@ -1114,23 +1114,93 @@ export default {
### `useColorMode` {#use-color-mode}
A React hook to access the color context. This context contains functions for setting light and dark mode and exposes boolean variable, indicating which mode is currently in use.
A React hook to access the color context. This context contains functions for selecting light/dark/system mode and exposes the current color mode and the choice from the user. The color mode values **should not be used for dynamic content rendering** (see below).
Usage example:
```jsx
import React from 'react';
// highlight-next-line
import {useColorMode} from '@docusaurus/theme-common';
const Example = () => {
// highlight-next-line
const {colorMode, setColorMode} = useColorMode();
const MyColorModeButton = () => {
// highlight-start
const {
colorMode, // the "effective" color mode, never null
colorModeChoice, // the color mode chosen by the user, can be null
setColorMode, // set the color mode chosen by the user
} = useColorMode();
// highlight-end
return <h1>Dark mode is now {colorMode === 'dark' ? 'on' : 'off'}</h1>;
return (
<button
onClick={() => {
const nextColorMode = colorModeChoice === 'dark' ? 'light' : 'dark';
setColorMode(nextColorMode);
}}>
Toggle color mode
</button>
);
};
```
Attributes:
- `colorMode: 'light' | 'dark'`: The effective color mode currently applied to the UI. It cannot be `null`.
- `colorModeChoice: 'light' | 'dark' | null`: The color mode explicitly chosen by the user. It can be `null` if user has not made any choice yet, or if they reset their choice to the system/default value.
- `setColorMode(colorModeChoice: 'light' | 'dark' | null, options: {persist: boolean}): void`: A function to call when the user explicitly chose a color mode. `null` permits to reset the choice to the system/default value. By default, the choice is persisted in `localStorage` and restored on page reload, but you can opt out with `{persist: false}`.
:::warning
Don't use `colorMode` and `colorModeChoice` while rendering React components. Doing so is likely to produce [FOUC](https://en.wikipedia.org/wiki/Flash_of_unstyled_content), layout shifts and [React hydration](https://18.react.dev/reference/react-dom/client/hydrateRoot) mismatches if you use them to render JSX content dynamically.
However, these values are safe to use **after React hydration**, in `useEffect` and event listeners, like in the `MyColorModeButton` example above.
If you need to render content dynamically depending on the current theme, the only way to avoid FOUC, layout shifts and hydration mismatch is to rely on CSS selectors to render content dynamically, based on the `html` data attributes that we set before the page displays anything:
```html
<html data-theme="<light | dark>" data-theme-choice="<light | dark | system>">
<!-- content -->
</html>
```
```css
[data-theme='light']
[data-theme='dark']
[data-theme-choice='light']
[data-theme-choice='dark']
[data-theme-choice='system']
```
<details>
<summary>Why are `colorMode` and `colorModeChoice` unsafe when rendering?</summary>
To understand the problem, you need to understand how [React hydration](https://18.react.dev/reference/react-dom/client/hydrateRoot) works.
During the static site generation phase, Docusaurus doesn't know what the user color mode choice is, and `useColorMode()` returns the following static values:
- `colorMode = themeConfig.colorMode.defaultMode`
- `colorModeChoice = null`
During the very first React client-side render (the hydration), React must produce the exact same HTML markup, and will also use these static values.
The correct `colorMode` and `colorModeChoice` values will only be provided in the second React render.
Typically, the following component will lead to **React hydration mismatches**. The label may switch from `light` to `dark` while React hydrates, leading to a confusing user experience.
```jsx
import {useColorMode} from '@docusaurus/theme-common';
const DisplayCurrentColorMode = () => {
const {colorMode} = useColorMode();
return <span>{colorMode}</span>;
};
```
</details>
:::
:::note
The component calling `useColorMode` must be a child of the `Layout` component.