(null);
const [customized, setCustomized] = React.useState(false);
- const theme = customized ? customTheme : undefined;
+ const theme = customized ? customTheme : defaultTheme;
return (
diff --git a/docs/src/components/productMaterial/MaterialHero.tsx b/docs/src/components/productMaterial/MaterialHero.tsx
index d3c0468da9a9db..82a5d00b28df2f 100644
--- a/docs/src/components/productMaterial/MaterialHero.tsx
+++ b/docs/src/components/productMaterial/MaterialHero.tsx
@@ -197,6 +197,7 @@ const { palette: lightPalette } = getDesignTokens('light');
const { palette: darkPalette } = getDesignTokens('dark');
const customTheme = extendTheme({
cssVarPrefix: 'hero',
+ colorSchemeSelector: 'data-mui-color-scheme',
colorSchemes: {
light: {
palette: {
diff --git a/docs/translations/translations.json b/docs/translations/translations.json
index f1dd6c1724591a..1ee4ef05be344a 100644
--- a/docs/translations/translations.json
+++ b/docs/translations/translations.json
@@ -239,8 +239,8 @@
"/material-ui/customization/transitions": "Transitions",
"/material-ui/customization/css-variables": "Css variables",
"/material-ui/customization/css-theme-variables/overview": "Overview",
- "/material-ui/customization/css-theme-variables/usage": "Usage",
- "/material-ui/customization/css-theme-variables/configuration": "Configuration",
+ "/material-ui/customization/css-theme-variables/usage": "Basic usage",
+ "/material-ui/customization/css-theme-variables/configuration": "Advanced configuration",
"/material-ui/guides": "How-to guides",
"/material-ui/guides/minimizing-bundle-size": "Minimizing bundle size",
"/material-ui/guides/server-rendering": "Server rendering",
diff --git a/packages/mui-joy/src/styles/defaultTheme.test.js b/packages/mui-joy/src/styles/defaultTheme.test.js
index c595c01b9c6b91..23830e85b241e3 100644
--- a/packages/mui-joy/src/styles/defaultTheme.test.js
+++ b/packages/mui-joy/src/styles/defaultTheme.test.js
@@ -5,7 +5,6 @@ describe('defaultTheme', () => {
it('the output contains required fields', () => {
Object.keys(defaultTheme).forEach((field) => {
expect([
- 'attribute',
'colorSchemeSelector',
'defaultColorScheme',
'breakpoints',
diff --git a/packages/mui-joy/src/styles/extendTheme.test.js b/packages/mui-joy/src/styles/extendTheme.test.js
index 29a7da8c536391..a3e6728ad1a44d 100644
--- a/packages/mui-joy/src/styles/extendTheme.test.js
+++ b/packages/mui-joy/src/styles/extendTheme.test.js
@@ -280,6 +280,9 @@ describe('extendTheme', () => {
it('applyStyles', () => {
const attribute = 'data-custom-color-scheme';
+ const customTheme2 = extendTheme({
+ colorSchemeSelector: attribute,
+ });
let darkStyles = {};
const Test = styled('div')(({ theme }) => {
darkStyles = theme.applyStyles('dark', {
@@ -289,7 +292,7 @@ describe('extendTheme', () => {
});
render(
-
+
,
);
diff --git a/packages/mui-joy/src/styles/extendTheme.ts b/packages/mui-joy/src/styles/extendTheme.ts
index 34052c61f99992..2986ecff1d4254 100644
--- a/packages/mui-joy/src/styles/extendTheme.ts
+++ b/packages/mui-joy/src/styles/extendTheme.ts
@@ -12,7 +12,7 @@ import {
} from '@mui/system';
import cssContainerQueries from '@mui/system/cssContainerQueries';
import { unstable_applyStyles as applyStyles } from '@mui/system/createTheme';
-import { prepareTypographyVars } from '@mui/system/cssVars';
+import { prepareTypographyVars, createGetColorSchemeSelector } from '@mui/system/cssVars';
import { createUnarySpacing } from '@mui/system/spacing';
import defaultSxConfig from './sxConfig';
import colors from '../colors';
@@ -83,6 +83,23 @@ export interface CssVarsThemeOptions extends Partial2Level {
* // { ..., typography: { body1: { fontSize: 'var(--fontSize-md)' } }, ... }
*/
cssVarPrefix?: string;
+ /**
+ * The strategy to generate CSS variables
+ *
+ * @example 'media'
+ * Generate CSS variables using [prefers-color-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
+ *
+ * @example '.mode-%s'
+ * Generate CSS variables within a class .mode-light, .mode-dark
+ *
+ * @example '[data-mode-%s]'
+ * Generate CSS variables within a data attribute [data-mode-light], [data-mode-dark]
+ */
+ colorSchemeSelector?: 'media' | 'class' | 'data' | string;
+ /**
+ * @default 'light'
+ */
+ defaultColorScheme?: DefaultColorScheme | ExtendedColorScheme;
direction?: 'ltr' | 'rtl';
focus?: Partial;
typography?: Partial;
@@ -100,26 +117,6 @@ export interface CssVarsThemeOptions extends Partial2Level {
* value = 'var(--test)'
*/
shouldSkipGeneratingVar?: (keys: string[], value: string | number) => boolean;
- /**
- * If provided, it will be used to create a selector for the color scheme.
- * This is useful if you want to use class or data-* attributes to apply the color scheme.
- *
- * The callback receives the colorScheme with the possible values of:
- * - undefined: the selector for tokens that are not color scheme dependent
- * - string: the selector for the color scheme
- *
- * @example
- * // class selector
- * (colorScheme) => colorScheme !== 'light' ? `.theme-${colorScheme}` : ":root"
- *
- * @example
- * // data-* attribute selector
- * (colorScheme) => colorScheme !== 'light' ? `[data-theme="${colorScheme}"`] : ":root"
- */
- getSelector?: (
- colorScheme: SupportedColorScheme | undefined,
- css: Record,
- ) => string | Record;
}
export const createGetCssVar = (cssVarPrefix = 'joy') =>
@@ -133,7 +130,7 @@ export default function extendTheme(themeOptions?: CssVarsThemeOptions): Theme {
components: componentsInput,
variants: variantsInput,
shouldSkipGeneratingVar = defaultShouldSkipGeneratingVar,
- getSelector,
+ colorSchemeSelector = 'data-joy-color-scheme',
...scalesInput
} = themeOptions || {};
const getCssVar = createGetCssVar(cssVarPrefix);
@@ -563,6 +560,7 @@ export default function extendTheme(themeOptions?: CssVarsThemeOptions): Theme {
: defaultScales;
let theme = {
+ colorSchemeSelector,
colorSchemes,
defaultColorScheme: 'light',
...mergedScales,
@@ -608,7 +606,7 @@ export default function extendTheme(themeOptions?: CssVarsThemeOptions): Theme {
getCssVar,
spacing: getSpacingVal(spacing),
font: { ...prepareTypographyVars(mergedScales.typography), ...mergedScales.font },
- } as unknown as Theme & { attribute: string; colorSchemeSelector: string }; // Need type casting due to module augmentation inside the repo
+ } as unknown as Theme & { colorSchemeSelector: string }; // Need type casting due to module augmentation inside the repo
theme = cssContainerQueries(theme);
/**
@@ -651,28 +649,17 @@ export default function extendTheme(themeOptions?: CssVarsThemeOptions): Theme {
// ===============================================================
// Create `theme.vars` that contain `var(--*)` as values
// ===============================================================
- const parserConfig = {
+ const parserConfig: Parameters>[1] = {
prefix: cssVarPrefix,
+ colorSchemeSelector,
+ disableCssColorScheme: true,
shouldSkipGeneratingVar,
- getSelector:
- getSelector ||
- ((colorScheme) => {
- if (theme.defaultColorScheme === colorScheme) {
- return `${theme.colorSchemeSelector}, [${theme.attribute}="${colorScheme}"]`;
- }
- if (colorScheme) {
- return `[${theme.attribute}="${colorScheme}"]`;
- }
- return theme.colorSchemeSelector;
- }),
};
const { vars, generateThemeVars, generateStyleSheets } = prepareCssVars(
theme,
parserConfig,
);
- theme.attribute = 'data-joy-color-scheme';
- theme.colorSchemeSelector = ':root';
theme.vars = vars;
theme.generateThemeVars = generateThemeVars;
theme.generateStyleSheets = generateStyleSheets;
@@ -691,8 +678,7 @@ export default function extendTheme(themeOptions?: CssVarsThemeOptions): Theme {
theme: this,
});
};
- theme.getColorSchemeSelector = (colorScheme: SupportedColorScheme) =>
- `[${theme.attribute}="${colorScheme}"] &`;
+ theme.getColorSchemeSelector = createGetColorSchemeSelector(colorSchemeSelector);
const createVariantInput = { getCssVar, palette: theme.colorSchemes.light.palette };
theme.variants = deepmerge(
diff --git a/packages/mui-joy/src/styles/shouldSkipGeneratingVar.ts b/packages/mui-joy/src/styles/shouldSkipGeneratingVar.ts
index d61b8f695cc5b8..2f1e39c95ad0aa 100644
--- a/packages/mui-joy/src/styles/shouldSkipGeneratingVar.ts
+++ b/packages/mui-joy/src/styles/shouldSkipGeneratingVar.ts
@@ -1,6 +1,6 @@
export default function shouldSkipGeneratingVar(keys: string[]) {
return (
- !!keys[0].match(/^(typography|variants|breakpoints)$/) ||
+ !!keys[0].match(/^(colorSchemeSelector|typography|variants|breakpoints)$/) ||
!!keys[0].match(/sxConfig$/) || // ends with sxConfig
(keys[0] === 'palette' && !!keys[1]?.match(/^(mode)$/)) ||
(keys[0] === 'focus' && keys[1] !== 'thickness')
diff --git a/packages/mui-material/src/Paper/Paper.js b/packages/mui-material/src/Paper/Paper.js
index abb07e7966bed5..3b42dce0db18dd 100644
--- a/packages/mui-material/src/Paper/Paper.js
+++ b/packages/mui-material/src/Paper/Paper.js
@@ -115,7 +115,7 @@ const Paper = React.forwardRef(function Paper(inProps, ref) {
...(variant === 'elevation' && {
'--Paper-shadow': (theme.vars || theme).shadows[elevation],
...(theme.vars && {
- '--Paper-overlay': theme.overlays?.[elevation],
+ '--Paper-overlay': theme.vars.overlays?.[elevation],
}),
...(!theme.vars &&
theme.palette.mode === 'dark' && {
diff --git a/packages/mui-material/src/styles/CssVarsProvider.test.js b/packages/mui-material/src/styles/CssVarsProvider.test.js
index e77b138bf4e680..6dfd78746c7213 100644
--- a/packages/mui-material/src/styles/CssVarsProvider.test.js
+++ b/packages/mui-material/src/styles/CssVarsProvider.test.js
@@ -2,7 +2,7 @@ import * as React from 'react';
import { expect } from 'chai';
import { createRenderer, screen } from '@mui/internal-test-utils';
import Box from '@mui/material/Box';
-import { CssVarsProvider, useTheme } from '@mui/material/styles';
+import { CssVarsProvider, extendTheme, useTheme } from '@mui/material/styles';
describe('[Material UI] CssVarsProvider', () => {
let originalMatchmedia;
@@ -57,7 +57,7 @@ describe('[Material UI] CssVarsProvider', () => {
}
render(
-
+
,
);
@@ -328,7 +328,7 @@ describe('[Material UI] CssVarsProvider', () => {
}
const { getByTestId } = render(
-
+
({
themeId: THEME_ID,
theme: defaultTheme,
- attribute: defaultConfig.attribute,
colorSchemeStorageKey: defaultConfig.colorSchemeStorageKey,
modeStorageKey: defaultConfig.modeStorageKey,
defaultColorScheme: {
diff --git a/packages/mui-material/src/styles/createGetSelector.ts b/packages/mui-material/src/styles/createGetSelector.ts
index b5eff828009ad9..2f79dc98c99cab 100644
--- a/packages/mui-material/src/styles/createGetSelector.ts
+++ b/packages/mui-material/src/styles/createGetSelector.ts
@@ -2,8 +2,7 @@ import excludeVariablesFromRoot from './excludeVariablesFromRoot';
export default <
T extends {
- attribute: string;
- colorSchemeSelector: string;
+ colorSchemeSelector?: 'media' | 'class' | 'data' | string;
colorSchemes?: Record;
defaultColorScheme?: string;
cssVarPrefix?: string;
@@ -12,6 +11,18 @@ export default <
theme: T,
) =>
(colorScheme: keyof T['colorSchemes'] | undefined, css: Record) => {
+ const selector = theme.colorSchemeSelector;
+ let rule = selector;
+ if (selector === 'class') {
+ rule = '.%s';
+ }
+ if (selector === 'data') {
+ rule = '[data-%s]';
+ }
+ if (selector?.startsWith('data-') && !selector.includes('%s')) {
+ // 'data-mui-color-scheme' -> '[data-mui-color-scheme="%s"]'
+ rule = `[${selector}="%s"]`;
+ }
if (theme.defaultColorScheme === colorScheme) {
if (colorScheme === 'dark') {
const excludedVariables: typeof css = {};
@@ -19,15 +30,27 @@ export default <
excludedVariables[cssVar] = css[cssVar];
delete css[cssVar];
});
- return {
- [`[${theme.attribute}="${String(colorScheme)}"]`]: excludedVariables,
- [theme.colorSchemeSelector!]: css,
- };
+ if (rule === 'media') {
+ return {
+ ':root': css,
+ '@media (prefers-color-scheme: dark) { :root': excludedVariables,
+ };
+ }
+ if (rule) {
+ return {
+ [rule.replace('%s', colorScheme)]: excludedVariables,
+ ':root': css,
+ };
+ }
+ return { ':root': { ...css, ...excludedVariables } };
+ }
+ } else if (colorScheme) {
+ if (rule === 'media') {
+ return `@media (prefers-color-scheme: ${String(colorScheme)}) { :root`;
+ }
+ if (rule) {
+ return rule.replace('%s', String(colorScheme));
}
- return `${theme.colorSchemeSelector}, [${theme.attribute}="${String(colorScheme)}"]`;
- }
- if (colorScheme) {
- return `[${theme.attribute}="${String(colorScheme)}"]`;
}
- return theme.colorSchemeSelector;
+ return ':root';
};
diff --git a/packages/mui-material/src/styles/excludeVariablesFromRoot.ts b/packages/mui-material/src/styles/excludeVariablesFromRoot.ts
index 6041d0f1547fcf..1c558562693f46 100644
--- a/packages/mui-material/src/styles/excludeVariablesFromRoot.ts
+++ b/packages/mui-material/src/styles/excludeVariablesFromRoot.ts
@@ -1,5 +1,5 @@
/**
- * @internal These variables should not appear in the :root stylesheet when the `defaultMode="dark"`
+ * @internal These variables should not appear in the :root stylesheet when the `defaultColorScheme="dark"`
*/
const excludeVariablesFromRoot = (cssVarPrefix?: string) => [
...[...Array(24)].map(
diff --git a/packages/mui-material/src/styles/extendTheme.d.ts b/packages/mui-material/src/styles/extendTheme.d.ts
index 6a01d0dae59322..c6eafa4422101e 100644
--- a/packages/mui-material/src/styles/extendTheme.d.ts
+++ b/packages/mui-material/src/styles/extendTheme.d.ts
@@ -276,6 +276,10 @@ export interface ColorSystem {
}
export interface CssVarsThemeOptions extends Omit {
+ /**
+ * @default 'light'
+ */
+ defaultColorScheme?: SupportedColorScheme;
/**
* Prefix of the generated CSS variables
* @default 'mui'
@@ -288,27 +292,27 @@ export interface CssVarsThemeOptions extends Omit>;
+ colorSchemes?: Partial> &
+ (ExtendedColorScheme extends string ? Record : {});
/**
- * If provided, it will be used to create a selector for the color scheme.
- * This is useful if you want to use class or data-* attributes to apply the color scheme.
+ * The strategy to generate CSS variables
*
- * The callback receives the colorScheme with the possible values of:
- * - undefined: the selector for tokens that are not color scheme dependent
- * - string: the selector for the color scheme
+ * @example 'media'
+ * Generate CSS variables using [prefers-color-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
*
- * @example
- * // class selector
- * (colorScheme) => colorScheme !== 'light' ? `.theme-${colorScheme}` : ":root"
+ * @example '.mode-%s'
+ * Generate CSS variables within a class .mode-light, .mode-dark
*
- * @example
- * // data-* attribute selector
- * (colorScheme) => colorScheme !== 'light' ? `[data-theme="${colorScheme}"`] : ":root"
+ * @example '[data-mode-%s]'
+ * Generate CSS variables within a data attribute [data-mode-light], [data-mode-dark]
+ */
+ colorSchemeSelector?: 'media' | 'class' | 'data' | string;
+ /**
+ * If `true`, the CSS color-scheme will not be set.
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/color-scheme
+ * @default false
*/
- getSelector?: (
- colorScheme: SupportedColorScheme | undefined,
- css: Record,
- ) => string | Record;
+ disableCssColorScheme?: boolean;
/**
* A function to determine if the key, value should be attached as CSS Variable
* `keys` is an array that represents the object path keys.
@@ -427,6 +431,7 @@ export type ThemeCssVar = OverridableStringUnion<
*/
export interface CssVarsTheme extends ColorSystem {
colorSchemes: Record;
+ colorSchemeSelector: 'media' | 'class' | 'data' | string;
cssVarPrefix: string;
vars: ThemeVars;
getCssVar: (field: ThemeCssVar, ...vars: ThemeCssVar[]) => string;
diff --git a/packages/mui-material/src/styles/extendTheme.js b/packages/mui-material/src/styles/extendTheme.js
index 96511b8d9a6d9a..7901d3d223cbfa 100644
--- a/packages/mui-material/src/styles/extendTheme.js
+++ b/packages/mui-material/src/styles/extendTheme.js
@@ -1,7 +1,12 @@
+import MuiError from '@mui/internal-babel-macros/MuiError.macro';
import deepmerge from '@mui/utils/deepmerge';
import { unstable_createGetCssVar as systemCreateGetCssVar, createSpacing } from '@mui/system';
import { createUnarySpacing } from '@mui/system/spacing';
-import { prepareCssVars, prepareTypographyVars } from '@mui/system/cssVars';
+import {
+ prepareCssVars,
+ prepareTypographyVars,
+ createGetColorSchemeSelector,
+} from '@mui/system/cssVars';
import styleFunctionSx, {
unstable_defaultSxConfig as defaultSxConfig,
} from '@mui/system/styleFunctionSx';
@@ -90,56 +95,110 @@ const silent = (fn) => {
export const createGetCssVar = (cssVarPrefix = 'mui') => systemCreateGetCssVar(cssVarPrefix);
+function getOpacity(mode) {
+ return {
+ inputPlaceholder: mode === 'dark' ? 0.5 : 0.42,
+ inputUnderline: mode === 'dark' ? 0.7 : 0.42,
+ switchTrackDisabled: mode === 'dark' ? 0.2 : 0.12,
+ switchTrack: mode === 'dark' ? 0.3 : 0.38,
+ };
+}
+function getOverlays(mode) {
+ return mode === 'dark' ? defaultDarkOverlays : [];
+}
+
+function attachColorScheme(colorSchemes, scheme, restTheme, colorScheme) {
+ if (!scheme) {
+ return undefined;
+ }
+ scheme = scheme === true ? {} : scheme;
+ const mode = colorScheme === 'dark' ? 'dark' : 'light';
+ const { palette, ...muiTheme } = createThemeWithoutVars({
+ ...restTheme,
+ palette: {
+ mode,
+ ...scheme?.palette,
+ },
+ });
+ colorSchemes[colorScheme] = {
+ ...scheme,
+ palette,
+ opacity: {
+ ...getOpacity(mode),
+ ...scheme?.opacity,
+ },
+ overlays: scheme?.overlays || getOverlays(mode),
+ };
+ return muiTheme;
+}
+
+/**
+ * A default `extendTheme` comes with a single color scheme, either `light` or `dark` based on the `defaultColorScheme`.
+ * This is better suited for apps that only need a single color scheme.
+ *
+ * To enable built-in `light` and `dark` color schemes, either:
+ * 1. provide a `colorSchemeSelector` to define how the color schemes will change.
+ * 2. provide `colorSchemes.dark` will set `colorSchemeSelector: 'media'` by default.
+ */
export default function extendTheme(options = {}, ...args) {
const {
- colorSchemes: colorSchemesInput = {},
+ colorSchemes: colorSchemesInput = { light: true },
+ defaultColorScheme: defaultColorSchemeInput,
+ disableCssColorScheme = false,
cssVarPrefix = 'mui',
shouldSkipGeneratingVar = defaultShouldSkipGeneratingVar,
- getSelector,
+ colorSchemeSelector: selector = colorSchemesInput.light && colorSchemesInput.dark
+ ? 'media'
+ : undefined,
...input
} = options;
+ const firstColorScheme = Object.keys(colorSchemesInput)[0];
+ const defaultColorScheme =
+ defaultColorSchemeInput ||
+ (colorSchemesInput.light && firstColorScheme !== 'light' ? 'light' : firstColorScheme);
const getCssVar = createGetCssVar(cssVarPrefix);
+ const {
+ [defaultColorScheme]: defaultSchemeInput,
+ light: builtInLight,
+ dark: builtInDark,
+ ...customColorSchemes
+ } = colorSchemesInput;
+ const colorSchemes = { ...customColorSchemes };
+ let defaultScheme = defaultSchemeInput;
- const { palette: lightPalette, ...muiTheme } = createThemeWithoutVars({
- ...input,
- ...(colorSchemesInput.light && { palette: colorSchemesInput.light?.palette }),
- });
- const { palette: darkPalette } = createThemeWithoutVars({
- palette: { mode: 'dark', ...colorSchemesInput.dark?.palette },
- });
+ // For built-in light and dark color schemes, ensure that the value is valid if they are the default color scheme.
+ if (
+ (defaultColorScheme === 'dark' && !('dark' in colorSchemesInput)) ||
+ (defaultColorScheme === 'light' && !('light' in colorSchemesInput))
+ ) {
+ defaultScheme = true;
+ }
+
+ if (!defaultScheme) {
+ throw new MuiError(
+ 'MUI: The provided `colorSchemes.%s` to the `extendTheme` function is either missing or invalid.',
+ defaultColorScheme,
+ );
+ }
+
+ // Create the palette for the default color scheme, either `light`, `dark`, or custom color scheme.
+ const muiTheme = attachColorScheme(colorSchemes, defaultScheme, input, defaultColorScheme);
+
+ if (builtInLight && !colorSchemes.light) {
+ attachColorScheme(colorSchemes, builtInLight, undefined, 'light');
+ }
+
+ if (builtInDark && !colorSchemes.dark) {
+ attachColorScheme(colorSchemes, builtInDark, undefined, 'dark');
+ }
let theme = {
- defaultColorScheme: 'light',
+ defaultColorScheme,
...muiTheme,
cssVarPrefix,
+ colorSchemeSelector: selector,
getCssVar,
- colorSchemes: {
- ...colorSchemesInput,
- light: {
- ...colorSchemesInput.light,
- palette: lightPalette,
- opacity: {
- inputPlaceholder: 0.42,
- inputUnderline: 0.42,
- switchTrackDisabled: 0.12,
- switchTrack: 0.38,
- ...colorSchemesInput.light?.opacity,
- },
- overlays: colorSchemesInput.light?.overlays || [],
- },
- dark: {
- ...colorSchemesInput.dark,
- palette: darkPalette,
- opacity: {
- inputPlaceholder: 0.5,
- inputUnderline: 0.7,
- switchTrackDisabled: 0.2,
- switchTrack: 0.3,
- ...colorSchemesInput.dark?.opacity,
- },
- overlays: colorSchemesInput.dark?.overlays || defaultDarkOverlays,
- },
- },
+ colorSchemes,
font: { ...prepareTypographyVars(muiTheme.typography), ...muiTheme.font },
spacing: getSpacingVal(input.spacing),
};
@@ -155,10 +214,11 @@ export default function extendTheme(options = {}, ...args) {
};
// attach black & white channels to common node
- if (key === 'light') {
+ if (palette.mode === 'light') {
setColor(palette.common, 'background', '#fff');
setColor(palette.common, 'onBackground', '#000');
- } else {
+ }
+ if (palette.mode === 'dark') {
setColor(palette.common, 'background', '#000');
setColor(palette.common, 'onBackground', '#fff');
}
@@ -182,7 +242,7 @@ export default function extendTheme(options = {}, ...args) {
'TableCell',
'Tooltip',
]);
- if (key === 'light') {
+ if (palette.mode === 'light') {
setColor(palette.Alert, 'errorColor', safeDarken(palette.error.light, 0.6));
setColor(palette.Alert, 'infoColor', safeDarken(palette.info.light, 0.6));
setColor(palette.Alert, 'successColor', safeDarken(palette.success.light, 0.6));
@@ -194,22 +254,22 @@ export default function extendTheme(options = {}, ...args) {
setColor(
palette.Alert,
'errorFilledColor',
- silent(() => lightPalette.getContrastText(palette.error.main)),
+ silent(() => palette.getContrastText(palette.error.main)),
);
setColor(
palette.Alert,
'infoFilledColor',
- silent(() => lightPalette.getContrastText(palette.info.main)),
+ silent(() => palette.getContrastText(palette.info.main)),
);
setColor(
palette.Alert,
'successFilledColor',
- silent(() => lightPalette.getContrastText(palette.success.main)),
+ silent(() => palette.getContrastText(palette.success.main)),
);
setColor(
palette.Alert,
'warningFilledColor',
- silent(() => lightPalette.getContrastText(palette.warning.main)),
+ silent(() => palette.getContrastText(palette.warning.main)),
);
setColor(palette.Alert, 'errorStandardBg', safeLighten(palette.error.light, 0.9));
setColor(palette.Alert, 'infoStandardBg', safeLighten(palette.info.light, 0.9));
@@ -251,7 +311,7 @@ export default function extendTheme(options = {}, ...args) {
setColor(
palette.SnackbarContent,
'color',
- silent(() => lightPalette.getContrastText(snackbarContentBackground)),
+ silent(() => palette.getContrastText(snackbarContentBackground)),
);
setColor(
palette.SpeedDialAction,
@@ -270,7 +330,8 @@ export default function extendTheme(options = {}, ...args) {
setColor(palette.Switch, 'warningDisabledColor', safeLighten(palette.warning.main, 0.62));
setColor(palette.TableCell, 'border', safeLighten(safeAlpha(palette.divider, 1), 0.88));
setColor(palette.Tooltip, 'bg', safeAlpha(palette.grey[700], 0.92));
- } else {
+ }
+ if (palette.mode === 'dark') {
setColor(palette.Alert, 'errorColor', safeLighten(palette.error.light, 0.6));
setColor(palette.Alert, 'infoColor', safeLighten(palette.info.light, 0.6));
setColor(palette.Alert, 'successColor', safeLighten(palette.success.light, 0.6));
@@ -282,22 +343,22 @@ export default function extendTheme(options = {}, ...args) {
setColor(
palette.Alert,
'errorFilledColor',
- silent(() => darkPalette.getContrastText(palette.error.dark)),
+ silent(() => palette.getContrastText(palette.error.dark)),
);
setColor(
palette.Alert,
'infoFilledColor',
- silent(() => darkPalette.getContrastText(palette.info.dark)),
+ silent(() => palette.getContrastText(palette.info.dark)),
);
setColor(
palette.Alert,
'successFilledColor',
- silent(() => darkPalette.getContrastText(palette.success.dark)),
+ silent(() => palette.getContrastText(palette.success.dark)),
);
setColor(
palette.Alert,
'warningFilledColor',
- silent(() => darkPalette.getContrastText(palette.warning.dark)),
+ silent(() => palette.getContrastText(palette.warning.dark)),
);
setColor(palette.Alert, 'errorStandardBg', safeDarken(palette.error.light, 0.9));
setColor(palette.Alert, 'infoStandardBg', safeDarken(palette.info.light, 0.9));
@@ -341,7 +402,7 @@ export default function extendTheme(options = {}, ...args) {
setColor(
palette.SnackbarContent,
'color',
- silent(() => darkPalette.getContrastText(snackbarContentBackground)),
+ silent(() => palette.getContrastText(snackbarContentBackground)),
);
setColor(
palette.SpeedDialAction,
@@ -420,12 +481,11 @@ export default function extendTheme(options = {}, ...args) {
const parserConfig = {
prefix: cssVarPrefix,
+ disableCssColorScheme,
shouldSkipGeneratingVar,
- getSelector: getSelector || defaultGetSelector(theme),
+ getSelector: defaultGetSelector(theme),
};
const { vars, generateThemeVars, generateStyleSheets } = prepareCssVars(theme, parserConfig);
- theme.attribute = 'data-mui-color-scheme';
- theme.colorSchemeSelector = ':root';
theme.vars = vars;
Object.entries(theme.colorSchemes[theme.defaultColorScheme]).forEach(([key, value]) => {
theme[key] = value;
@@ -435,7 +495,7 @@ export default function extendTheme(options = {}, ...args) {
theme.generateSpacing = function generateSpacing() {
return createSpacing(input.spacing, createUnarySpacing(this));
};
- theme.getColorSchemeSelector = (colorScheme) => `[${theme.attribute}="${colorScheme}"] &`;
+ theme.getColorSchemeSelector = createGetColorSchemeSelector(selector);
theme.spacing = theme.generateSpacing();
theme.shouldSkipGeneratingVar = shouldSkipGeneratingVar;
theme.unstable_sxConfig = {
diff --git a/packages/mui-material/src/styles/extendTheme.spec.ts b/packages/mui-material/src/styles/extendTheme.spec.ts
index ae8722c899c93b..fc32973b6f0f16 100644
--- a/packages/mui-material/src/styles/extendTheme.spec.ts
+++ b/packages/mui-material/src/styles/extendTheme.spec.ts
@@ -18,3 +18,9 @@ theme.getCssVar('');
theme.getCssVar('custom-color');
// @ts-expect-error
theme.getCssVar('palette-primary-main', '');
+
+// dark only application
+extendTheme({ colorSchemes: { dark: true } });
+
+// built-in light and dark modes
+extendTheme({ colorSchemes: { light: true, dark: true } });
diff --git a/packages/mui-material/src/styles/extendTheme.test.js b/packages/mui-material/src/styles/extendTheme.test.js
index f2ac5fa8dc9502..3f8d8f5e9ced3f 100644
--- a/packages/mui-material/src/styles/extendTheme.test.js
+++ b/packages/mui-material/src/styles/extendTheme.test.js
@@ -1,8 +1,9 @@
import * as React from 'react';
import { expect } from 'chai';
+import sinon from 'sinon';
import { createRenderer } from '@mui/internal-test-utils';
import Button from '@mui/material/Button';
-import { CssVarsProvider, extendTheme, styled } from '@mui/material/styles';
+import { CssVarsProvider, extendTheme } from '@mui/material/styles';
import { deepOrange, green } from '@mui/material/colors';
describe('extendTheme', () => {
@@ -32,10 +33,69 @@ describe('extendTheme', () => {
window.matchMedia = originalMatchmedia;
});
- it('should have a colorSchemes', () => {
+ it('should have a light colorScheme by default', () => {
const theme = extendTheme();
expect(typeof extendTheme).to.equal('function');
expect(typeof theme.colorSchemes).to.equal('object');
+ expect(typeof theme.colorSchemes.light).to.equal('object');
+ expect(theme.colorSchemes.dark).to.equal(undefined);
+ });
+
+ it('should have a light as a default colorScheme regardless of key order', () => {
+ const theme = extendTheme({
+ colorSchemes: { dark: true, light: true },
+ });
+ expect(theme.defaultColorScheme).to.equal('light');
+ });
+
+ it('should have "media" colorSchemeSelector', () => {
+ const theme = extendTheme({ colorSchemeSelector: 'media' });
+ expect(theme.colorSchemeSelector).to.equal('media');
+ });
+
+ it('should have CSS color-scheme by default', () => {
+ const theme = extendTheme();
+ sinon.assert.match(theme.generateStyleSheets()[1], {
+ ':root': {
+ colorScheme: 'light',
+ },
+ });
+ });
+
+ it('should have CSS color-scheme: dark', () => {
+ const theme = extendTheme({ defaultColorScheme: 'dark' });
+ sinon.assert.match(theme.generateStyleSheets()[1], {
+ ':root': {
+ colorScheme: 'dark',
+ },
+ });
+ });
+
+ it('should throw error if the default color scheme is invalid', () => {
+ expect(() =>
+ extendTheme({ colorSchemes: { dark: false }, defaultColorScheme: 'dark' }),
+ ).to.throw(
+ 'MUI: The provided `colorSchemes.dark` to the `extendTheme` function is either missing or invalid.',
+ );
+ });
+
+ it('should throw error if the default color scheme is missing', () => {
+ expect(() => extendTheme({ defaultColorScheme: 'paper' })).to.throw(
+ 'MUI: The provided `colorSchemes.paper` to the `extendTheme` function is either missing or invalid.',
+ );
+ });
+
+ it('should not attach to `colorSchemes` if the provided scheme is invalid', () => {
+ const theme = extendTheme({ colorSchemes: { dark: null, light: true } });
+ expect(theme.colorSchemes.dark).to.equal(undefined);
+ });
+
+ it('disableCssColorScheme should remove CSS color-scheme', () => {
+ const theme = extendTheme({ disableCssColorScheme: true });
+ expect(theme.generateStyleSheets()[1][':root'].colorScheme).to.equal(undefined);
+
+ const theme2 = extendTheme({ defaultColorScheme: 'dark', disableCssColorScheme: true });
+ expect(theme2.generateStyleSheets()[1][':root'].colorScheme).to.equal(undefined);
});
it('should have the custom color schemes', () => {
@@ -52,59 +112,61 @@ describe('extendTheme', () => {
it('should generate color channels', () => {
const theme = extendTheme();
- expect(theme.colorSchemes.dark.palette.background.defaultChannel).to.equal('18 18 18');
+
expect(theme.colorSchemes.light.palette.background.defaultChannel).to.equal('255 255 255');
- expect(theme.colorSchemes.dark.palette.background.paperChannel).to.equal('18 18 18');
expect(theme.colorSchemes.light.palette.background.paperChannel).to.equal('255 255 255');
- expect(theme.colorSchemes.dark.palette.primary.mainChannel).to.equal('144 202 249');
- expect(theme.colorSchemes.dark.palette.primary.darkChannel).to.equal('66 165 245');
- expect(theme.colorSchemes.dark.palette.primary.lightChannel).to.equal('227 242 253');
- expect(theme.colorSchemes.dark.palette.primary.contrastTextChannel).to.equal('0 0 0');
-
expect(theme.colorSchemes.light.palette.primary.mainChannel).to.equal('25 118 210');
expect(theme.colorSchemes.light.palette.primary.darkChannel).to.equal('21 101 192');
expect(theme.colorSchemes.light.palette.primary.lightChannel).to.equal('66 165 245');
expect(theme.colorSchemes.light.palette.primary.contrastTextChannel).to.equal('255 255 255');
- expect(theme.colorSchemes.dark.palette.secondary.mainChannel).to.equal('206 147 216');
- expect(theme.colorSchemes.dark.palette.secondary.darkChannel).to.equal('171 71 188');
- expect(theme.colorSchemes.dark.palette.secondary.lightChannel).to.equal('243 229 245');
- expect(theme.colorSchemes.dark.palette.secondary.contrastTextChannel).to.equal('0 0 0');
-
expect(theme.colorSchemes.light.palette.secondary.mainChannel).to.equal('156 39 176');
expect(theme.colorSchemes.light.palette.secondary.darkChannel).to.equal('123 31 162');
expect(theme.colorSchemes.light.palette.secondary.lightChannel).to.equal('186 104 200');
expect(theme.colorSchemes.light.palette.secondary.contrastTextChannel).to.equal('255 255 255');
- expect(theme.colorSchemes.dark.palette.text.primaryChannel).to.equal('255 255 255');
- expect(theme.colorSchemes.dark.palette.text.secondaryChannel).to.equal('255 255 255');
-
expect(theme.colorSchemes.light.palette.text.primaryChannel).to.equal('0 0 0');
expect(theme.colorSchemes.light.palette.text.secondaryChannel).to.equal('0 0 0');
- expect(theme.colorSchemes.dark.palette.dividerChannel).to.equal('255 255 255');
-
expect(theme.colorSchemes.light.palette.dividerChannel).to.equal('0 0 0');
- expect(theme.colorSchemes.dark.palette.action.activeChannel).to.equal('255 255 255');
expect(theme.colorSchemes.light.palette.action.activeChannel).to.equal('0 0 0');
- expect(theme.colorSchemes.dark.palette.action.selectedChannel).to.equal('255 255 255');
expect(theme.colorSchemes.light.palette.action.selectedChannel).to.equal('0 0 0');
});
+ it('should generate dark color channels', () => {
+ const theme = extendTheme({ defaultColorScheme: 'dark' });
+
+ expect(theme.colorSchemes.dark.palette.background.defaultChannel).to.equal('18 18 18');
+
+ expect(theme.colorSchemes.dark.palette.background.paperChannel).to.equal('18 18 18');
+
+ expect(theme.colorSchemes.dark.palette.primary.mainChannel).to.equal('144 202 249');
+ expect(theme.colorSchemes.dark.palette.primary.darkChannel).to.equal('66 165 245');
+ expect(theme.colorSchemes.dark.palette.primary.lightChannel).to.equal('227 242 253');
+ expect(theme.colorSchemes.dark.palette.primary.contrastTextChannel).to.equal('0 0 0');
+
+ expect(theme.colorSchemes.dark.palette.secondary.mainChannel).to.equal('206 147 216');
+ expect(theme.colorSchemes.dark.palette.secondary.darkChannel).to.equal('171 71 188');
+ expect(theme.colorSchemes.dark.palette.secondary.lightChannel).to.equal('243 229 245');
+ expect(theme.colorSchemes.dark.palette.secondary.contrastTextChannel).to.equal('0 0 0');
+
+ expect(theme.colorSchemes.dark.palette.text.primaryChannel).to.equal('255 255 255');
+ expect(theme.colorSchemes.dark.palette.text.secondaryChannel).to.equal('255 255 255');
+
+ expect(theme.colorSchemes.dark.palette.dividerChannel).to.equal('255 255 255');
+
+ expect(theme.colorSchemes.dark.palette.action.activeChannel).to.equal('255 255 255');
+
+ expect(theme.colorSchemes.dark.palette.action.selectedChannel).to.equal('255 255 255');
+ });
+
it('should generate common background, onBackground channels', () => {
const theme = extendTheme({
colorSchemes: {
- dark: {
- palette: {
- common: {
- onBackground: '#f9f9f9', // this should not be overridden
- },
- },
- },
light: {
palette: {
common: {
@@ -118,7 +180,21 @@ describe('extendTheme', () => {
expect(theme.colorSchemes.light.palette.common.backgroundChannel).to.equal('249 249 249');
expect(theme.colorSchemes.light.palette.common.onBackground).to.equal('#000');
expect(theme.colorSchemes.light.palette.common.onBackgroundChannel).to.equal('0 0 0');
+ });
+ it('should generate dark common background, onBackground channels', () => {
+ const theme = extendTheme({
+ defaultColorScheme: 'dark',
+ colorSchemes: {
+ dark: {
+ palette: {
+ common: {
+ onBackground: '#f9f9f9', // this should not be overridden
+ },
+ },
+ },
+ },
+ });
expect(theme.colorSchemes.dark.palette.common.background).to.equal('#000');
expect(theme.colorSchemes.dark.palette.common.backgroundChannel).to.equal('0 0 0');
expect(theme.colorSchemes.dark.palette.common.onBackground).to.equal('#f9f9f9');
@@ -212,6 +288,10 @@ describe('extendTheme', () => {
switchTrackDisabled: 0.12,
switchTrack: 0.38,
});
+ });
+
+ it('should provide the default dark opacities', () => {
+ const theme = extendTheme({ defaultColorScheme: 'dark' });
expect(theme.colorSchemes.dark.opacity).to.deep.equal({
inputPlaceholder: 0.5,
inputUnderline: 0.7,
@@ -228,6 +308,18 @@ describe('extendTheme', () => {
inputPlaceholder: 1,
},
},
+ },
+ });
+ expect(theme.colorSchemes.light.opacity).to.deep.include({
+ inputPlaceholder: 1,
+ inputUnderline: 0.42,
+ });
+ });
+
+ it('should allow overriding of the default dark opacities', () => {
+ const theme = extendTheme({
+ defaultColorScheme: 'dark',
+ colorSchemes: {
dark: {
opacity: {
inputPlaceholder: 0.2,
@@ -235,10 +327,6 @@ describe('extendTheme', () => {
},
},
});
- expect(theme.colorSchemes.light.opacity).to.deep.include({
- inputPlaceholder: 1,
- inputUnderline: 0.42,
- });
expect(theme.colorSchemes.dark.opacity).to.deep.include({
inputPlaceholder: 0.2,
inputUnderline: 0.7,
@@ -250,6 +338,10 @@ describe('extendTheme', () => {
it('should provide the default array', () => {
const theme = extendTheme();
expect(theme.colorSchemes.light.overlays).to.have.length(0);
+ });
+
+ it('should provide the default array for dark', () => {
+ const theme = extendTheme({ defaultColorScheme: 'dark' });
expect(theme.colorSchemes.dark.overlays).to.have.length(25);
expect(theme.colorSchemes.dark.overlays[0]).to.equal(undefined);
@@ -260,7 +352,10 @@ describe('extendTheme', () => {
it('should override the array as expected', () => {
const overlays = Array(24).fill('none');
- const theme = extendTheme({ colorSchemes: { dark: { overlays } } });
+ const theme = extendTheme({
+ defaultColorScheme: 'dark',
+ colorSchemes: { dark: { overlays } },
+ });
expect(theme.colorSchemes.dark.overlays).to.equal(overlays);
});
});
@@ -605,47 +700,112 @@ describe('extendTheme', () => {
});
});
- it('should use the right selector with applyStyles', function test() {
- const attribute = 'data-custom-color-scheme';
- let darkStyles = {};
- const Test = styled('div')(({ theme }) => {
- darkStyles = theme.applyStyles('dark', {
- backgroundColor: 'rgba(0, 0, 0, 0)',
- });
- return null;
+ describe('dark color scheme only', () => {
+ it('should use dark as default color scheme', () => {
+ expect(extendTheme({ colorSchemes: { dark: true } }).defaultColorScheme).to.deep.equal(
+ 'dark',
+ );
});
- render(
-
-
- ,
- );
+ it('should not have colorSchemeSelector', () => {
+ expect(extendTheme({ colorSchemes: { dark: true } }).colorSchemeSelector).to.deep.equal(
+ undefined,
+ );
+ });
- expect(darkStyles).to.deep.equal({
- [`*:where([${attribute}="dark"]) &`]: {
- backgroundColor: 'rgba(0, 0, 0, 0)',
- },
+ it('should have dark palette and not light color scheme', () => {
+ const theme = extendTheme({ colorSchemes: { dark: true } });
+ expect(theme.colorSchemes.dark.palette.text.primary).to.equal('#fff');
+ expect(theme.colorSchemes.light).to.equal(undefined);
});
});
- it("should `generateStyleSheets` based on the theme's attribute and colorSchemeSelector", () => {
- const theme = extendTheme();
+ describe('light and dark color schemes', () => {
+ it('should use prefers-color-scheme (`media`) by default', () => {
+ const theme = extendTheme({ colorSchemes: { light: true, dark: true } });
+ expect(theme.generateStyleSheets().flatMap((sheet) => Object.keys(sheet))).to.deep.equal([
+ ':root',
+ ':root',
+ '@media (prefers-color-scheme: dark) { :root',
+ ]);
+ });
+
+ it('[media] should use prefers-color-scheme for styling', () => {
+ const theme = extendTheme({ colorSchemes: { light: true, dark: true } });
+
+ expect(theme.getColorSchemeSelector('light')).to.equal(
+ '@media (prefers-color-scheme: light)',
+ );
+ expect(theme.getColorSchemeSelector('dark')).to.equal('@media (prefers-color-scheme: dark)');
+ });
+
+ it('[media] should use prefers-color-scheme with dark as default', () => {
+ const theme = extendTheme({
+ colorSchemes: { light: true, dark: true },
+ defaultColorScheme: 'dark',
+ });
+ expect(theme.generateStyleSheets().flatMap((sheet) => Object.keys(sheet))).to.deep.equal([
+ ':root',
+ ':root',
+ '@media (prefers-color-scheme: dark) { :root', // this key targets excluded variables for dark
+ '@media (prefers-color-scheme: light) { :root',
+ ]);
+ });
+
+ it('should use default class selector', () => {
+ const theme = extendTheme({
+ colorSchemes: { light: true, dark: true },
+ colorSchemeSelector: 'class',
+ });
+ expect(theme.generateStyleSheets().flatMap((sheet) => Object.keys(sheet))).to.deep.equal([
+ ':root',
+ ':root',
+ '.dark',
+ ]);
+ });
- expect(theme.generateStyleSheets().flatMap((sheet) => Object.keys(sheet))).to.deep.equal([
- ':root',
- ':root, [data-mui-color-scheme="light"]',
- '[data-mui-color-scheme="dark"]',
- ]);
-
- theme.attribute = 'data-custom-color-scheme';
- theme.colorSchemeSelector = '.root';
- theme.defaultColorScheme = 'dark';
-
- expect(theme.generateStyleSheets().flatMap((sheet) => Object.keys(sheet))).to.deep.equal([
- '.root',
- '[data-custom-color-scheme="dark"]',
- '.root',
- '[data-custom-color-scheme="light"]',
- ]);
+ it('should use a custom class selector', () => {
+ const theme = extendTheme({
+ colorSchemes: { light: true, dark: true },
+ colorSchemeSelector: '.mode-%s',
+ });
+ expect(theme.generateStyleSheets().flatMap((sheet) => Object.keys(sheet))).to.deep.equal([
+ ':root',
+ ':root',
+ '.mode-dark',
+ ]);
+ });
+
+ it('should use default data selector for styling', () => {
+ const theme = extendTheme({
+ colorSchemes: { light: true, dark: true },
+ colorSchemeSelector: 'data',
+ });
+
+ expect(theme.getColorSchemeSelector('light')).to.equal('[data-light] &');
+ expect(theme.getColorSchemeSelector('dark')).to.equal('[data-dark] &');
+ });
+
+ it('should use data attribute selector', () => {
+ const theme = extendTheme({
+ colorSchemes: { light: true, dark: true },
+ colorSchemeSelector: '[data-theme-%s]',
+ });
+ expect(theme.generateStyleSheets().flatMap((sheet) => Object.keys(sheet))).to.deep.equal([
+ ':root',
+ ':root',
+ '[data-theme-dark]',
+ ]);
+ });
+
+ it('should use data attribute for styling', () => {
+ const theme = extendTheme({
+ colorSchemes: { light: true, dark: true },
+ colorSchemeSelector: '[data-theme-%s]',
+ });
+
+ expect(theme.getColorSchemeSelector('light')).to.equal('[data-theme-light] &');
+ expect(theme.getColorSchemeSelector('dark')).to.equal('[data-theme-dark] &');
+ });
});
});
diff --git a/packages/mui-material/src/styles/shouldSkipGeneratingVar.ts b/packages/mui-material/src/styles/shouldSkipGeneratingVar.ts
index 0146d320943c88..0bd931028287cd 100644
--- a/packages/mui-material/src/styles/shouldSkipGeneratingVar.ts
+++ b/packages/mui-material/src/styles/shouldSkipGeneratingVar.ts
@@ -1,6 +1,8 @@
export default function shouldSkipGeneratingVar(keys: string[]) {
return (
- !!keys[0].match(/(cssVarPrefix|typography|mixins|breakpoints|direction|transitions)/) ||
+ !!keys[0].match(
+ /(cssVarPrefix|colorSchemeSelector|typography|mixins|breakpoints|direction|transitions)/,
+ ) ||
!!keys[0].match(/sxConfig$/) || // ends with sxConfig
(keys[0] === 'palette' && !!keys[1]?.match(/(mode|contrastThreshold|tonalOffset)/))
);
diff --git a/packages/mui-system/src/InitColorSchemeScript/InitColorSchemeScript.test.js b/packages/mui-system/src/InitColorSchemeScript/InitColorSchemeScript.test.js
index b3e63d899c1d63..50f9ae3330d383 100644
--- a/packages/mui-system/src/InitColorSchemeScript/InitColorSchemeScript.test.js
+++ b/packages/mui-system/src/InitColorSchemeScript/InitColorSchemeScript.test.js
@@ -61,6 +61,55 @@ describe('InitColorSchemeScript', () => {
expect(document.documentElement.getAttribute('data-mui-baz-scheme')).to.equal('flash');
});
+ it('should switch between light and dark with class attribute', () => {
+ storage[DEFAULT_MODE_STORAGE_KEY] = 'light';
+ storage[`${DEFAULT_COLOR_SCHEME_STORAGE_KEY}-light`] = 'foo';
+ storage[`${DEFAULT_COLOR_SCHEME_STORAGE_KEY}-dark`] = 'bar';
+
+ const { container, rerender } = render();
+ eval(container.firstChild.textContent);
+ expect(document.documentElement.classList.value).to.equal('mode-foo');
+
+ storage[DEFAULT_MODE_STORAGE_KEY] = 'dark';
+ rerender();
+ eval(container.firstChild.textContent);
+ expect(document.documentElement.classList.value).to.equal('mode-bar');
+
+ document.documentElement.classList.remove('mode-bar'); // cleanup
+ });
+
+ it('should switch between light and dark with data-%s attribute', () => {
+ storage[DEFAULT_MODE_STORAGE_KEY] = 'light';
+ storage[`${DEFAULT_COLOR_SCHEME_STORAGE_KEY}-light`] = 'foo';
+ storage[`${DEFAULT_COLOR_SCHEME_STORAGE_KEY}-dark`] = 'bar';
+
+ const { container, rerender } = render();
+ eval(container.firstChild.textContent);
+ expect(document.documentElement.getAttribute('data-mode-foo')).to.equal('');
+ expect(document.documentElement.getAttribute('data-mode-bar')).to.equal(null);
+
+ storage[DEFAULT_MODE_STORAGE_KEY] = 'dark';
+ rerender();
+ eval(container.firstChild.textContent);
+ expect(document.documentElement.getAttribute('data-mode-bar')).to.equal('');
+ expect(document.documentElement.getAttribute('data-mode-foo')).to.equal(null);
+ });
+
+ it('should switch between light and dark with data="%s" attribute', () => {
+ storage[DEFAULT_MODE_STORAGE_KEY] = 'light';
+ storage[`${DEFAULT_COLOR_SCHEME_STORAGE_KEY}-light`] = 'foo';
+ storage[`${DEFAULT_COLOR_SCHEME_STORAGE_KEY}-dark`] = 'bar';
+
+ const { container, rerender } = render();
+ eval(container.firstChild.textContent);
+ expect(document.documentElement.getAttribute('data-mode')).to.equal('foo');
+
+ storage[DEFAULT_MODE_STORAGE_KEY] = 'dark';
+ rerender();
+ eval(container.firstChild.textContent);
+ expect(document.documentElement.getAttribute('data-mode')).to.equal('bar');
+ });
+
it('should set `dark` color scheme to body', () => {
storage[DEFAULT_MODE_STORAGE_KEY] = 'dark';
storage[`${DEFAULT_COLOR_SCHEME_STORAGE_KEY}-dark`] = 'bar';
@@ -90,19 +139,11 @@ describe('InitColorSchemeScript', () => {
expect(document.documentElement.getAttribute(DEFAULT_ATTRIBUTE)).to.equal('bright');
});
- it('defaultMode: `dark`', () => {
- const { container } = render();
- eval(container.firstChild.textContent);
- expect(document.documentElement.getAttribute(DEFAULT_ATTRIBUTE)).to.equal('dark');
- });
-
- describe('defaultMode: `system`', () => {
+ describe('system preference', () => {
it('should set dark color scheme to body, given prefers-color-scheme is `dark`', () => {
window.matchMedia = createMatchMedia(true);
- const { container } = render(
- ,
- );
+ const { container } = render();
eval(container.firstChild.textContent);
expect(document.documentElement.getAttribute(DEFAULT_ATTRIBUTE)).to.equal('trueDark');
});
@@ -110,9 +151,7 @@ describe('InitColorSchemeScript', () => {
it('should set light color scheme to body, given prefers-color-scheme is NOT `dark`', () => {
window.matchMedia = createMatchMedia(false);
- const { container } = render(
- ,
- );
+ const { container } = render();
eval(container.firstChild.textContent);
expect(document.documentElement.getAttribute(DEFAULT_ATTRIBUTE)).to.equal('yellow');
});
diff --git a/packages/mui-system/src/InitColorSchemeScript/InitColorSchemeScript.tsx b/packages/mui-system/src/InitColorSchemeScript/InitColorSchemeScript.tsx
index ff7892f469fcde..3d537ed6087971 100644
--- a/packages/mui-system/src/InitColorSchemeScript/InitColorSchemeScript.tsx
+++ b/packages/mui-system/src/InitColorSchemeScript/InitColorSchemeScript.tsx
@@ -8,11 +8,6 @@ export const DEFAULT_COLOR_SCHEME_STORAGE_KEY = 'color-scheme';
export const DEFAULT_ATTRIBUTE = 'data-color-scheme';
export interface InitColorSchemeScriptProps {
- /**
- * The mode to be used for the first visit
- * @default 'light'
- */
- defaultMode?: 'light' | 'dark' | 'system';
/**
* The default color scheme to be used on the light mode
* @default 'light'
@@ -41,6 +36,9 @@ export interface InitColorSchemeScriptProps {
/**
* DOM attribute for applying color scheme
* @default 'data-color-scheme'
+ *
+ * @example '.mode-%s' // for class based color scheme
+ * @example '[data-mode-%s]' // for data-attribute without '='
*/
attribute?: string;
/**
@@ -51,7 +49,6 @@ export interface InitColorSchemeScriptProps {
export default function InitColorSchemeScript(options?: InitColorSchemeScriptProps) {
const {
- defaultMode = 'light',
defaultLightColorScheme = 'light',
defaultDarkColorScheme = 'dark',
modeStorageKey = DEFAULT_MODE_STORAGE_KEY,
@@ -60,6 +57,25 @@ export default function InitColorSchemeScript(options?: InitColorSchemeScriptPro
colorSchemeNode = 'document.documentElement',
nonce,
} = options || {};
+ let setter = '';
+ if (attribute.startsWith('.')) {
+ const selector = attribute.substring(1);
+ setter += `${colorSchemeNode}.classList.remove('${selector}'.replace('%s', light), '${selector}'.replace('%s', dark));
+ ${colorSchemeNode}.classList.add('${selector}'.replace('%s', colorScheme));`;
+ }
+ const matches = attribute.match(/\[([^\]]+)\]/); // case [data-color-scheme=%s] or [data-color-scheme]
+ if (matches) {
+ const [attr, value] = matches[1].split('=');
+ if (!value) {
+ setter += `${colorSchemeNode}.removeAttribute('${attr}'.replace('%s', light));
+ ${colorSchemeNode}.removeAttribute('${attr}'.replace('%s', dark));`;
+ }
+ setter += `
+ ${colorSchemeNode}.setAttribute('${attr}'.replace('%s', colorScheme), ${value ? `${value}.replace('%s', colorScheme)` : '""'});`;
+ } else {
+ setter += `${colorSchemeNode}.setAttribute('${attribute}', colorScheme);`;
+ }
+
return (