diff --git a/.prettierignore b/.prettierignore
index 1f599c7c1a..a4e8eef74d 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -14,4 +14,5 @@ packages/docusaurus-plugin-content-pages/lib/
packages/docusaurus-plugin-debug/lib/
packages/docusaurus-plugin-sitemap/lib/
packages/docusaurus-plugin-ideal-image/lib/
+packages/docusaurus-theme-classic/lib/
__fixtures__
diff --git a/packages/docusaurus-theme-classic/src/index.js b/packages/docusaurus-theme-classic/src/index.js
index 44275d476a..5a1c26ad4e 100644
--- a/packages/docusaurus-theme-classic/src/index.js
+++ b/packages/docusaurus-theme-classic/src/index.js
@@ -20,41 +20,49 @@ const ContextReplacementPlugin = requireFromDocusaurusCore(
// Need to be inlined to prevent dark mode FOUC
// Make sure that the 'storageKey' is the same as the one in `/theme/hooks/useTheme.js`
const storageKey = 'theme';
-const noFlash = (defaultDarkMode) => `(function() {
- var defaultDarkMode = ${defaultDarkMode};
+const noFlashColorMode = ({defaultMode, respectPrefersColorScheme}) => {
+ return `(function() {
+ var defaultMode = '${defaultMode}';
+ var respectPrefersColorScheme = ${respectPrefersColorScheme};
function setDataThemeAttribute(theme) {
document.documentElement.setAttribute('data-theme', theme);
}
- function getPreferredTheme() {
+ function getStoredTheme() {
var theme = null;
try {
theme = localStorage.getItem('${storageKey}');
} catch (err) {}
-
return theme;
}
- var darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
-
- var preferredTheme = getPreferredTheme();
- if (preferredTheme !== null) {
- setDataThemeAttribute(preferredTheme);
- } else if (darkQuery.matches || defaultDarkMode) {
- setDataThemeAttribute('dark');
+ var storedTheme = getStoredTheme();
+ if (storedTheme !== null) {
+ setDataThemeAttribute(storedTheme);
+ } 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');
+ }
}
})();`;
+};
module.exports = function (context, options) {
const {
siteConfig: {themeConfig},
} = context;
- const {
- disableDarkMode = false,
- defaultDarkMode = false,
- prism: {additionalLanguages = []} = {},
- } = themeConfig || {};
+ const {colorMode, prism: {additionalLanguages = []} = {}} = themeConfig || {};
const {customCss} = options || {};
return {
@@ -97,9 +105,6 @@ module.exports = function (context, options) {
},
injectHtmlTags() {
- if (disableDarkMode) {
- return {};
- }
return {
preBodyTags: [
{
@@ -107,7 +112,7 @@ module.exports = function (context, options) {
attributes: {
type: 'text/javascript',
},
- innerHTML: noFlash(defaultDarkMode),
+ innerHTML: noFlashColorMode(colorMode),
},
],
};
@@ -130,8 +135,26 @@ const NavbarLinkSchema = Joi.object({
.xor('href', 'to')
.id('navbarLinkSchema');
+const ColorModeSchema = Joi.object({
+ defaultMode: Joi.string().equal('dark', 'light').default('light'),
+ disableSwitch: Joi.bool().default(false),
+ respectPrefersColorScheme: Joi.bool().default(false),
+}).default({
+ defaultMode: 'light',
+ disableSwitch: false,
+ respectPrefersColorScheme: false,
+});
+
const ThemeConfigSchema = Joi.object({
- disableDarkMode: Joi.bool().default(false),
+ disableDarkMode: Joi.any().forbidden(false).messages({
+ 'any.unknown':
+ 'disableDarkMode theme config is deprecated. Please use the new colorMode attribute. You likely want: config.themeConfig.colorMode.disableSwitch = true',
+ }),
+ defaultDarkMode: Joi.any().forbidden(false).messages({
+ 'any.unknown':
+ 'defaultDarkMode theme config is deprecated. Please use the new colorMode attribute. You likely want: config.themeConfig.colorMode.defaultMode = "dark"',
+ }),
+ colorMode: ColorModeSchema,
image: Joi.string(),
announcementBar: Joi.object({
id: Joi.string(),
diff --git a/packages/docusaurus-theme-classic/src/theme/Navbar/index.tsx b/packages/docusaurus-theme-classic/src/theme/Navbar/index.tsx
index 798a4139f0..b7723923bf 100644
--- a/packages/docusaurus-theme-classic/src/theme/Navbar/index.tsx
+++ b/packages/docusaurus-theme-classic/src/theme/Navbar/index.tsx
@@ -185,7 +185,7 @@ function Navbar(): JSX.Element {
siteConfig: {
themeConfig: {
navbar: {title = '', links = [], hideOnScroll = false} = {},
- disableDarkMode = false,
+ colorMode: {disableSwitch: disableColorModeSwitch = false} = {},
},
},
isClient,
@@ -283,7 +283,7 @@ function Navbar(): JSX.Element {
{rightLinks.map((linkItem, i) => (
))}
- {!disableDarkMode && (
+ {!disableColorModeSwitch && (
{title}
)}
- {!disableDarkMode && sidebarShown && (
+ {!disableColorModeSwitch && sidebarShown && (
{
+ return theme === themes.dark ? themes.dark : themes.light;
+};
+
+const getInitialTheme = () => {
+ if (!ExecutionEnvironment.canUseDOM) {
+ return themes.light; // SSR: we don't care
+ }
+ return coerceToTheme(document.documentElement.getAttribute('data-theme'));
+};
+
+const storeTheme = (newTheme) => {
+ try {
+ localStorage.setItem('theme', coerceToTheme(newTheme));
+ } catch (err) {
+ console.error(err);
+ }
+};
+
const useTheme = (): {
isDarkTheme: boolean;
setLightTheme: () => void;
setDarkTheme: () => void;
} => {
const {
- siteConfig: {themeConfig: {disableDarkMode = false} = {}} = {},
+ siteConfig: {
+ themeConfig: {colorMode: {disableSwitch = false} = {}} = {},
+ } = {},
} = useDocusaurusContext();
- const [theme, setTheme] = useState(
- typeof document !== 'undefined'
- ? document.documentElement.getAttribute('data-theme')
- : themes.light,
- );
- const setThemeSyncWithLocalStorage = useCallback(
- (newTheme) => {
- try {
- localStorage.setItem('theme', newTheme);
- } catch (err) {
- console.error(err);
- }
- },
- [setTheme],
- );
+ const [theme, setTheme] = useState(getInitialTheme);
+
const setLightTheme = useCallback(() => {
setTheme(themes.light);
- setThemeSyncWithLocalStorage(themes.light);
+ storeTheme(themes.light);
}, []);
const setDarkTheme = useCallback(() => {
setTheme(themes.dark);
- setThemeSyncWithLocalStorage(themes.dark);
+ storeTheme(themes.dark);
}, []);
useEffect(() => {
- // @ts-expect-error: safe to set null as attribute
- document.documentElement.setAttribute('data-theme', theme);
+ document.documentElement.setAttribute('data-theme', coerceToTheme(theme));
}, [theme]);
useEffect(() => {
- if (disableDarkMode) {
+ if (disableSwitch) {
return;
}
try {
const localStorageTheme = localStorage.getItem('theme');
if (localStorageTheme !== null) {
- setTheme(localStorageTheme);
+ setTheme(coerceToTheme(localStorageTheme));
}
} catch (err) {
console.error(err);
@@ -67,7 +76,7 @@ const useTheme = (): {
}, [setTheme]);
useEffect(() => {
- if (disableDarkMode) {
+ if (disableSwitch) {
return;
}
diff --git a/packages/docusaurus/src/server/plugins/init.ts b/packages/docusaurus/src/server/plugins/init.ts
index 0cde6e9f16..11b0fee255 100644
--- a/packages/docusaurus/src/server/plugins/init.ts
+++ b/packages/docusaurus/src/server/plugins/init.ts
@@ -83,11 +83,11 @@ export default function initPlugins({
pluginModule.default?.validateOptions ?? pluginModule.validateOptions;
if (validateOptions) {
- const options = validateOptions({
+ const normalizedOptions = validateOptions({
validate,
options: pluginOptions,
});
- pluginOptions = options;
+ pluginOptions = normalizedOptions;
}
// support both commonjs and ES modules
@@ -96,10 +96,15 @@ export default function initPlugins({
pluginModule.validateThemeConfig;
if (validateThemeConfig) {
- validateThemeConfig({
+ const normalizedThemeConfig = validateThemeConfig({
validate: validateAndStrip,
themeConfig: context.siteConfig.themeConfig,
});
+
+ context.siteConfig.themeConfig = {
+ ...context.siteConfig.themeConfig,
+ ...normalizedThemeConfig,
+ };
}
return plugin(context, pluginOptions);
})
diff --git a/website/docs/lifecycle-apis.md b/website/docs/lifecycle-apis.md
index 68d43fd4f1..2baaeb3444 100644
--- a/website/docs/lifecycle-apis.md
+++ b/website/docs/lifecycle-apis.md
@@ -63,11 +63,11 @@ export function validateOptions({options, validate}) {
## `validateThemeConfig({themeConfig,validate})`
-Validate `themeConfig` for the plugins and theme. This method is called before the plugin is initialized.
+Return validated and normalized configuration for the theme.
### `themeConfig`
-`validateThemeConfig` is called with `themeConfig` provided in `docusaurus.config.js` for validation.
+`validateThemeConfig` is called with `themeConfig` provided in `docusaurus.config.js` for validation and normalization.
### `validate`
@@ -75,7 +75,7 @@ Validate `themeConfig` for the plugins and theme. This method is called before t
:::tip
-[Joi](https://www.npmjs.com/package/@hapi/joi) is recommended for validation and normalization of options.
+[Joi](https://www.npmjs.com/package/@hapi/joi) is recommended for validation and normalization of theme config.
:::
@@ -90,7 +90,8 @@ module.exports = function (context, options) {
};
module.exports.validateThemeConfig = ({themeConfig, validate}) => {
- validate(myValidationSchema, options);
+ const validatedThemeConfig = validate(myValidationSchema, options);
+ return validatedThemeConfig;
};
```
@@ -105,7 +106,8 @@ export default function (context, options) {
}
export function validateThemeConfig({themeConfig, validate}) {
- validate(myValidationSchema, options);
+ const validatedThemeConfig = validate(myValidationSchema, options);
+ return validatedThemeConfig;
}
```
diff --git a/website/docs/theme-classic.md b/website/docs/theme-classic.md
index 2f7f41340e..985be7049e 100644
--- a/website/docs/theme-classic.md
+++ b/website/docs/theme-classic.md
@@ -11,32 +11,43 @@ This section is a work in progress.
## Common
-### Dark mode
+### Color mode - dark mode
-To remove the ability to switch on dark mode, there is an option `themeConfig.disableDarkMode`, which is implicitly set to `false`.
+The classic theme provides by default light and dark mode support, with a navbar switch for the user.
-```js {4} title="docusaurus.config.js"
+It is possible to customize the color mode support with the following configuration:
+
+```js {6-15} title="docusaurus.config.js"
module.exports = {
// ...
themeConfig: {
- disableDarkMode: false,
+ // ...
+ colorMode: {
+ // "light" | "dark"
+ defaultMode: 'light',
+
+ // Hides the switch in the navbar
+ // Useful if you want to support a single color mode
+ disableSwitch: false,
+
+ // Should we use the prefers-color-scheme media-query,
+ // using user system preferences, instead of the hardcoded defaultMode
+ respectPrefersColorScheme: false,
+ },
// ...
},
-};
-```
-
-With the enabled `defaultDarkMode` option you could set dark mode by default. However, in this case, the user's preference will not be taken into account until they manually sets the desired mode via toggle in the navbar.
-
-```js {4} title="docusaurus.config.js"
-module.exports = {
// ...
- themeConfig: {
- defaultDarkMode: true,
- // ...
- },
};
```
+:::caution
+
+With `respectPrefersColorScheme: true`, the `defaultMode` is overridden by user system preferences.
+
+If you only want to support one color mode, you likely want to ignore user system preferences.
+
+:::
+
### Meta image
You can configure a default image that will be used for your meta tag, in particular `og:image` and `twitter:image`.
diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js
index 1567e83c7f..a1265a01fa 100644
--- a/website/docusaurus.config.js
+++ b/website/docusaurus.config.js
@@ -81,6 +81,11 @@ module.exports = {
],
],
themeConfig: {
+ colorMode: {
+ defaultMode: 'light',
+ disableSwitch: false,
+ respectPrefersColorScheme: true,
+ },
announcementBar: {
id: 'supportus',
content: