Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,83 @@ In the example below, all the components inside the `div` will always be dark:

</codeblock>

## Nested and multiple theme providers

A `ThemeProvider` writes the CSS theme variables to `:root`, so they apply to the whole page.
A nested or `scoped` provider sets them on its child element instead, so they only apply there.

### Nested providers

When a nested `ThemeProvider`'s variables differ from the outer theme, it adds a generated class to its child.
The class only sets the variables that differ, and it follows the outer light or dark mode:

```jsx
<ThemeProvider theme={appTheme}>
<Button>Uses appTheme</Button>
<ThemeProvider theme={promoTheme}>
<section>
<Button>Uses promoTheme</Button>
</section>
</ThemeProvider>
</ThemeProvider>
```

```html
<section class="mui-theme-1x2y3z">…</section>
```

```css
:root {
--mui-palette-primary-main: #1976d2;
}
.mui-theme-1x2y3z {
--mui-palette-primary-main: #9c27b0;
}
```

The provider doesn't render any element of its own, so the child has to be a single element that renders the `className` it receives.
With several children, a Fragment, or text, the variables aren't applied and an error is logged in development: wrap the children in one element, for example a `div`.
A nested provider whose theme has the same variables, for example one that only changes `components` defaults, accepts any children.

Dialogs, menus, popovers, tooltips, and other components that render in a portal apply the class to their own root, so they use the nested theme too.
For elements that other libraries portal, use the `useThemeScope()` hook to get the class:

```jsx
import { useThemeScope } from '@mui/material/styles';

function Toast() {
const themeScope = useThemeScope();
return ReactDOM.createPortal(<div {...themeScope}>…</div>, document.body);
}
```

### Sibling providers

Two `ThemeProvider`s that aren't nested in each other both write to `:root` and overwrite each other, which logs a warning in development.
Add the `scoped` prop to the one that should only apply to its child, like an embedded widget:

```jsx
<React.Fragment>
<ThemeProvider theme={appTheme}>
<App />
</ThemeProvider>
<ThemeProvider theme={widgetTheme} scoped="widget">
<div>
<Widget />
</div>
</ThemeProvider>
</React.Fragment>
```

A scoped provider sets all of its variables on its child element, and has its own light or dark mode.
To remember that mode across visits, give it a name: when `scoped` is a string, it's added to the class name and to the storage keys of the mode and color scheme, for example `mui-mode-widget`.
Without a name, the mode isn't stored, so it doesn't overwrite the mode of the page's provider.

:::info
A CSS variable that you override on `:root` doesn't reach the elements under a scoped provider that sets it, because the closest declaration wins.
Override it on the child element instead, for example with its own class.
:::

## Disabling CSS color scheme

By default, `createTheme()` attaches a [CSS `color-scheme` property](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/color-scheme) based on the palette mode.
Expand Down
4 changes: 4 additions & 0 deletions docs/data/material/customization/theming/theming.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ You can extend the outer theme by providing a function:

{{"component": "file://./demos/theme-nesting-extend/index.ts"}}

With CSS theme variables, a nested theme whose variables differ from the outer theme sets them on its child element, so the child has to be a single element.
This doesn't apply to a theme passed as a function, which still uses the outer theme's CSS variables.
See [Nested and multiple theme providers](/material-ui/customization/css-theme-variables/configuration/#nested-and-multiple-theme-providers) for details.

## CSS theme variables

To generate CSS variables from the theme, set `cssVariables` to `true` in the theme configuration and pass it to the `ThemeProvider`:
Expand Down
29 changes: 29 additions & 0 deletions docs/data/material/migration/upgrade-to-v10/upgrade-to-v10.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
# Upgrade to v10

<p class="description">This guide explains how to upgrade from Material UI v9 to v10.</p>

## Breaking changes

### Theme

#### Nested ThemeProviders with CSS theme variables

A nested `ThemeProvider` whose theme uses CSS theme variables now applies its theme.
Previously, its variables were ignored when it had the same `cssVarPrefix` as the outer theme, so its subtree kept the outer theme's colors.

When its variables differ from the outer theme, it sets them with a class on its child, so give it a single child element:

```diff
<ThemeProvider theme={promoTheme}>
- <CssBaseline />
- <Section />
+ <div>
+ <CssBaseline />
+ <Section />
+ </div>
</ThemeProvider>
```

With several children, a Fragment, or a child that doesn't render its `className`, the variables aren't applied, and the first two log an error in development.

Two `ThemeProvider`s that aren't nested in each other still overwrite each other's variables on `:root`, and now log a warning in development.
Add the `scoped` prop to the one that should only apply to its child, like an embedded widget.

See [Nested and multiple theme providers](/material-ui/customization/css-theme-variables/configuration/#nested-and-multiple-theme-providers) for details.
6 changes: 6 additions & 0 deletions packages/mui-material/src/Modal/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import clsx from 'clsx';
import HTMLElementType from '@mui/utils/HTMLElementType';
import elementAcceptingRef from '@mui/utils/elementAcceptingRef';
import composeClasses from '@mui/utils/composeClasses';
import { useThemeScope } from '@mui/system/cssVars';
import FocusTrap from '../Unstable_TrapFocus';
import Portal from '../Portal';
import { styled } from '../zero-styled';
Expand Down Expand Up @@ -147,17 +148,22 @@ const Modal = React.forwardRef(function Modal(inProps, ref) {

const externalForwardedProps = { slots, slotProps };

// The modal is portaled out of a scoped ThemeProvider's element, so it applies the scope itself.
const { className: themeScopeClassName, ...themeScopeAttributes } = useThemeScope();

const [RootSlot, rootProps] = useSlot('root', {
ref,
elementType: ModalRoot,
externalForwardedProps: {
...externalForwardedProps,
...themeScopeAttributes,
...other,
component,
},
getSlotProps: getRootProps,
ownerState,
className: clsx(
themeScopeClassName,
className,
classes?.root,
!ownerState.open && ownerState.exited && classes?.hidden,
Expand Down
6 changes: 6 additions & 0 deletions packages/mui-material/src/Popper/Popper.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use client';
import { SxProps } from '@mui/system';
import { useRtl } from '@mui/system/RtlProvider';
import { useThemeScope } from '@mui/system/cssVars';
import clsx from 'clsx';
import refType from '@mui/utils/refType';
import HTMLElementType from '@mui/utils/HTMLElementType';
import PropTypes from 'prop-types';
Expand Down Expand Up @@ -45,6 +47,8 @@ const Popper = React.forwardRef(function Popper(
ref: React.ForwardedRef<HTMLDivElement>,
) {
const isRtl = useRtl();
// The popper is portaled out of a scoped ThemeProvider's element, so it applies the scope itself.
const { className: themeScopeClassName, ...themeScopeAttributes } = useThemeScope();
const props = useDefaultProps({
props: inProps,
name: 'MuiPopper',
Expand Down Expand Up @@ -86,7 +90,9 @@ const Popper = React.forwardRef(function Popper(
direction={isRtl ? 'rtl' : 'ltr'}
slots={slots}
slotProps={slotProps}
{...themeScopeAttributes}
{...otherProps}
className={clsx(themeScopeClassName, otherProps.className) || undefined}
ref={ref}
/>
);
Expand Down
9 changes: 9 additions & 0 deletions packages/mui-material/src/styles/ThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ type ThemeProviderCssVariablesProps = CssThemeVariables extends { enabled: true
* @default false
*/
forceThemeRerender?: boolean | undefined;
/**
* If `true`, the CSS theme variables are set on its child element instead of `:root`,
* so they don't affect the rest of the page. Use it for a provider that isn't nested in another one,
* like an embedded widget. Nested providers are scoped automatically when their variables differ.
*
* A string is used as a readable part of the class name, and to suffix the storage keys.
* @default false
*/
scoped?: boolean | string | undefined;
}
: {};

Expand Down
Loading
Loading