From 0a4d49f85a6ee1cd313919567961d9e88e238b88 Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Thu, 2 Jul 2026 11:07:34 +0300 Subject: [PATCH 1/6] [docs] Add blog post for enhanceHighContrast --- .../palette/HighContrastShowcase.js | 326 ++++++++++++++++++ .../palette/HighContrastShowcase.tsx | 320 +++++++++++++++++ .../material/customization/palette/palette.md | 6 + docs/lib/sourcing.ts | 2 + ...igning-for-forced-colors-in-material-ui.js | 6 + ...igning-for-forced-colors-in-material-ui.md | 258 ++++++++++++++ docs/src/modules/components/TopLayoutBlog.js | 5 + 7 files changed, 923 insertions(+) create mode 100644 docs/data/material/customization/palette/HighContrastShowcase.js create mode 100644 docs/data/material/customization/palette/HighContrastShowcase.tsx create mode 100644 docs/pages/blog/designing-for-forced-colors-in-material-ui.js create mode 100644 docs/pages/blog/designing-for-forced-colors-in-material-ui.md diff --git a/docs/data/material/customization/palette/HighContrastShowcase.js b/docs/data/material/customization/palette/HighContrastShowcase.js new file mode 100644 index 00000000000000..0c9849d22c4fba --- /dev/null +++ b/docs/data/material/customization/palette/HighContrastShowcase.js @@ -0,0 +1,326 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import Accordion from '@mui/material/Accordion'; +import AccordionDetails from '@mui/material/AccordionDetails'; +import AccordionSummary from '@mui/material/AccordionSummary'; +import Autocomplete from '@mui/material/Autocomplete'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Checkbox from '@mui/material/Checkbox'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormGroup from '@mui/material/FormGroup'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import LinearProgress from '@mui/material/LinearProgress'; +import ListItemButton, { listItemButtonClasses } from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import MenuItem, { menuItemClasses } from '@mui/material/MenuItem'; +import MenuList from '@mui/material/MenuList'; +import NativeSelect from '@mui/material/NativeSelect'; +import Radio from '@mui/material/Radio'; +import Slider from '@mui/material/Slider'; +import Stack from '@mui/material/Stack'; +import Switch from '@mui/material/Switch'; +import TextField from '@mui/material/TextField'; +import ToggleButton from '@mui/material/ToggleButton'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import InboxIcon from '@mui/icons-material/Inbox'; +import StarIcon from '@mui/icons-material/Star'; +import { + ThemeProvider, + createTheme, + enhanceHighContrast, + useColorScheme, +} from '@mui/material/styles'; + +const autocompleteOptions = [ + { label: 'Disabled option', disabled: true }, + { label: 'Selected option' }, + { label: 'Focused option' }, +]; + +function Section(props) { + const { title, children } = props; + + return ( + + + {title} + + {children} + + ); +} + +Section.propTypes = { + children: PropTypes.node, + title: PropTypes.string.isRequired, +}; + +export default function HighContrastShowcase() { + const [enhanced, setEnhanced] = React.useState(false); + const { mode, systemMode } = useColorScheme(); + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', { + noSsr: true, + }); + const forcedColorsActive = useMediaQuery('(forced-colors: active)', { + noSsr: true, + }); + const docsMode = mode === 'system' ? systemMode : mode; + let paletteMode = prefersDarkMode ? 'dark' : 'light'; + + if (docsMode === 'dark' || docsMode === 'light') { + paletteMode = docsMode; + } + + const baseTheme = React.useMemo( + () => + createTheme({ + palette: { + mode: paletteMode, + }, + }), + [paletteMode], + ); + + const theme = React.useMemo(() => { + return enhanced ? enhanceHighContrast(baseTheme) : baseTheme; + }, [baseTheme, enhanced]); + + return ( + + + +
+ + Forced colors component showcase + + + In Chrome, open DevTools > More tools > Rendering, then set + forced-colors to active and prefers-color-scheme to + light or dark. Then toggle the enhancer to compare the fix. + +
+ + setEnhanced(event.target.checked)} + /> + } + label="Enhance high contrast" + /> + + Forced colors: {forcedColorsActive ? 'active' : 'inactive'} + + + Color scheme: {paletteMode} + + +
+ + +
+ + + + + + + Native select + + + + + + Disabled icon and label + + +
+ +
+ + + Selected item + + Focus-visible item + + + Selected + focus + + Disabled item + + + + + + + + + + + + + + + + + +
+ +
+ + } + label="Checkbox" + /> + } + label="Radio" + /> + } + label="Switch" + /> + + + + Disabled slider + + + + + Selected + Other + +
+ +
+ +
+ + Linear progress buffer + + +
+ + + + + + + }> + Disabled accordion + + + Disabled summary opacity should stay readable in forced colors. + + +
+
+ +
+ Boolean(option.disabled)} + renderInput={(params) => ( + + )} + sx={{ maxWidth: 320 }} + /> + + Shows disabled and selected option styling. + +
+ +
+ + + + + + + Use keyboard navigation to compare the visible focus outline. + +
+
+
+
+ ); +} diff --git a/docs/data/material/customization/palette/HighContrastShowcase.tsx b/docs/data/material/customization/palette/HighContrastShowcase.tsx new file mode 100644 index 00000000000000..833cd35adc8dd4 --- /dev/null +++ b/docs/data/material/customization/palette/HighContrastShowcase.tsx @@ -0,0 +1,320 @@ +import * as React from 'react'; +import Accordion from '@mui/material/Accordion'; +import AccordionDetails from '@mui/material/AccordionDetails'; +import AccordionSummary from '@mui/material/AccordionSummary'; +import Autocomplete from '@mui/material/Autocomplete'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Checkbox from '@mui/material/Checkbox'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormGroup from '@mui/material/FormGroup'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import LinearProgress from '@mui/material/LinearProgress'; +import ListItemButton, { listItemButtonClasses } from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import MenuItem, { menuItemClasses } from '@mui/material/MenuItem'; +import MenuList from '@mui/material/MenuList'; +import NativeSelect from '@mui/material/NativeSelect'; +import Radio from '@mui/material/Radio'; +import Slider from '@mui/material/Slider'; +import Stack from '@mui/material/Stack'; +import Switch from '@mui/material/Switch'; +import TextField from '@mui/material/TextField'; +import ToggleButton from '@mui/material/ToggleButton'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import InboxIcon from '@mui/icons-material/Inbox'; +import StarIcon from '@mui/icons-material/Star'; +import { + ThemeProvider, + createTheme, + enhanceHighContrast, + useColorScheme, +} from '@mui/material/styles'; + +const autocompleteOptions = [ + { label: 'Disabled option', disabled: true }, + { label: 'Selected option' }, + { label: 'Focused option' }, +]; + +function Section(props: { title: string; children: React.ReactNode }) { + const { title, children } = props; + + return ( + + + {title} + + {children} + + ); +} + +export default function HighContrastShowcase() { + const [enhanced, setEnhanced] = React.useState(false); + const { mode, systemMode } = useColorScheme(); + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', { + noSsr: true, + }); + const forcedColorsActive = useMediaQuery('(forced-colors: active)', { + noSsr: true, + }); + const docsMode = mode === 'system' ? systemMode : mode; + let paletteMode: 'light' | 'dark' = prefersDarkMode ? 'dark' : 'light'; + + if (docsMode === 'dark' || docsMode === 'light') { + paletteMode = docsMode; + } + + const baseTheme = React.useMemo( + () => + createTheme({ + palette: { + mode: paletteMode, + }, + }), + [paletteMode], + ); + + const theme = React.useMemo(() => { + return enhanced ? enhanceHighContrast(baseTheme) : baseTheme; + }, [baseTheme, enhanced]); + + return ( + + + +
+ + Forced colors component showcase + + + In Chrome, open DevTools > More tools > Rendering, then set + forced-colors to active and prefers-color-scheme to + light or dark. Then toggle the enhancer to compare the fix. + +
+ + setEnhanced(event.target.checked)} + /> + } + label="Enhance high contrast" + /> + + Forced colors: {forcedColorsActive ? 'active' : 'inactive'} + + + Color scheme: {paletteMode} + + +
+ + +
+ + + + + + + Native select + + + + + + Disabled icon and label + + +
+ +
+ + + Selected item + + Focus-visible item + + + Selected + focus + + Disabled item + + + + + + + + + + + + + + + + + +
+ +
+ + } + label="Checkbox" + /> + } + label="Radio" + /> + } + label="Switch" + /> + + + + Disabled slider + + + + + Selected + Other + +
+ +
+ +
+ + Linear progress buffer + + +
+ + + + + + + }> + Disabled accordion + + + Disabled summary opacity should stay readable in forced colors. + + +
+
+ +
+ Boolean(option.disabled)} + renderInput={(params) => ( + + )} + sx={{ maxWidth: 320 }} + /> + + Shows disabled and selected option styling. + +
+ +
+ + + + + + + Use keyboard navigation to compare the visible focus outline. + +
+
+
+
+ ); +} diff --git a/docs/data/material/customization/palette/palette.md b/docs/data/material/customization/palette/palette.md index 41c355636b06c9..cdb53d22b82a94 100644 --- a/docs/data/material/customization/palette/palette.md +++ b/docs/data/material/customization/palette/palette.md @@ -324,6 +324,12 @@ import { createTheme, enhanceHighContrast } from '@mui/material/styles'; const theme = enhanceHighContrast(createTheme()); ``` +Use the following demo to compare the affected component states with and without the theme enhancer. +In Chrome, open DevTools > More tools > Rendering, then set `forced-colors` to `active` and `prefers-color-scheme` to `light` or `dark`. +You can also use your operating system's high contrast or contrast theme settings. + +{{"demo": "HighContrastShowcase.js", "defaultCodeOpen": false}} + By default, it uses the [CSS system color keywords](https://www.w3.org/TR/css-color-4/#css-system-colors) (`Highlight`, `HighlightText`, `ButtonBorder`, etc.). You can override individual tokens with other [system colors](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/system-color) to match your brand or design requirements. When overriding paired tokens, choose values that the browser guarantees to contrast with each other — for example `SelectedItem` is always paired with `SelectedItemText`, and `Highlight` with `HighlightText`: diff --git a/docs/lib/sourcing.ts b/docs/lib/sourcing.ts index 5b53918949c8c4..8b51baed6ecf79 100644 --- a/docs/lib/sourcing.ts +++ b/docs/lib/sourcing.ts @@ -68,6 +68,8 @@ const ALLOWED_TAGS = [ 'Joy UI', 'MUI X', 'Toolpad', + // Subject tags + 'Accessibility', ]; export const getAllBlogPosts = () => { diff --git a/docs/pages/blog/designing-for-forced-colors-in-material-ui.js b/docs/pages/blog/designing-for-forced-colors-in-material-ui.js new file mode 100644 index 00000000000000..729581db054e0e --- /dev/null +++ b/docs/pages/blog/designing-for-forced-colors-in-material-ui.js @@ -0,0 +1,6 @@ +import TopLayoutBlog from 'docs/src/modules/components/TopLayoutBlog'; +import * as pageProps from './designing-for-forced-colors-in-material-ui.md?muiMarkdown'; + +export default function Page() { + return ; +} diff --git a/docs/pages/blog/designing-for-forced-colors-in-material-ui.md b/docs/pages/blog/designing-for-forced-colors-in-material-ui.md new file mode 100644 index 00000000000000..06591b74b20829 --- /dev/null +++ b/docs/pages/blog/designing-for-forced-colors-in-material-ui.md @@ -0,0 +1,258 @@ +--- +title: Windows High Contrast mode support in Material UI +description: Learn how Windows High Contrast maps to CSS system colors and why Material UI uses an opt-in theme enhancer for forced-colors support. +date: 2026-07-01T00:00:00.000Z +authors: ['silviuaavram'] +tags: ['Material UI', 'Accessibility', 'Guide'] +manualCard: false +--- + +## Windows High Contrast mode + +As described in the [Microsoft blog](https://blogs.windows.com/msedgedev/2020/09/17/styling-for-windows-high-contrast-with-new-standards-for-forced-colors/), the Windows High Contrast mode is an accessibility feature that is designed to increase text legibility and improve readability. It can be enabled at an Operating System level, by going to Settings > Ease of Access > High contrast, then 'Turn on high contrast'. + +The user can select system colors which are going to be applied, via the user theme, to their UI. In turn, web developers need to make sure these system colors are applied correctly to their apps, such that the resulting experience is correct. + +## CSS System Colors + +When Windows High Contrast Mode (WHCM) is turned on, complex color gradients, shadows, and background images are removed. Text is converted to a solid color that contrasts with the background. Buttons, borders, and menus are reduced to simple outlines and solid fills for clear visibility. + +These overrides are done using the [css system colors](https://www.w3.org/TR/css-color-4/#css-system-colors). They have OS defaults, but, as mentioned above, these could be changed by the user as well. System colors are meant to cover all cases where the OS needs a contrasting alternative to the colors it will replace. For example, `SelectedItem` and `SelectedItemText` are used to override a selected menu item's background and text colors. When doing an value override, the user should make sure that these colors contrast at least to the WCAG AA standard, but this part is out of scope for this article. Instead, we are going to focus on using these system colors in our components, to make sure the app in WHCM looks as expected. + +## CSS Features + +CSS provides a media query that detects whenever WHCM is active: + +### @media(forced-colors: $value) + +```css +@media (forced-colors: active) { + .highlighted-menu-item { + color: HighlightText; + background-color: Highlight; + } +} +``` + +Here, we are using the a duo of system colors to style a menu item's selected state. For native HTML elements, the browser will do that out of the box, so it's important that we do it ourselves for the custom built elements. + +### forced-color-adjust: $value + +Another CSS feature that is useful here is the [forced-color-adjust](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/forced-color-adjust) property. This is an escape hatch that should be used whenever we don't want the OS to apply WHCM styles on an elements and its children. + +In the example above, the styles are probably not going to be enough, as the OS will also apply a highlight style over the menu item's text, and that could look weird. Consequently, we can use the `forced-color-adjust: none` escape hatch to get rid of it. Just keep in mind that you are now in full control and need to make sure that the styles chosen for WHCM work well. + +## Chromium Flags + +Some useful Chromium features that allow developers to emulate and test WHCM are found in the DevTools' Rendering tab. These are: + +- `forced-colors`: when set to `active`, it emulates the WHCM inside the browser. +- `prefer-color-scheme`: force either `light` or `dark` mode, which is also useful for WHCM, since it's supposed to work in both color schemes. + +## WHCM with Material UI Components + +### The problem + +Material UI components generally behaved well in WHCM, but there were some corner cases that we missed. + +The fixes cover: + +- AccordionSummary disabled opacity. +- Autocomplete disabled options. +- Autocomplete focused and selected options. +- Checkbox disabled color. +- FilledInput error and disabled states. +- FormControlLabel disabled label. +- FormHelperText error and disabled states. +- FormLabel error and disabled states. +- Input error and disabled states. +- InputBase placeholder opacity. +- LinearProgress root, bar, and buffer visibility. +- MenuItem disabled, hover, focus-visible, and selected states. +- ListItemIcon inherited color. +- ListItemButton hover, focus-visible, and selected states. +- NativeSelect disabled icon. +- OutlinedInput error and disabled states. +- Radio disabled color. +- Slider disabled track and thumb. +- Switch disabled track and thumb. +- ButtonBase focus outline. +- Tooltip visible border. +- ToggleButton selected and selected hover states. + +If these cases would not have been properly visible within our default themes, it would have been a huge issue. Consequently, it was critical for us to address the gaps in WHCM. Lucky for us, Material UI provides many options to style components, so the real challenge was to choose the best one for the job. + +The following demo shows these affected states side by side. +In Chrome, open DevTools > More tools > Rendering, then set `forced-colors` to `active` and `prefers-color-scheme` to `light` or `dark`. +You can also use your operating system's high contrast or contrast theme settings. + +{{"demo": "../../data/material/customization/palette/HighContrastShowcase.js", "defaultCodeOpen": false}} + +### The options + +#### The `sx` prop + +```jsx + +``` + +The quickest way to patch a styling issue. However, we needed to fix the components on our end, instead of asking our consumers to patch all their components via `sx`. + +#### The `styled()` wrappers + +```jsx +const AppButton = styled(Button)(() => ({ + '@media (forced-colors: active)': { + borderColor: 'ButtonBorder', + }, + '&:focus-visible': { + '@media (forced-colors: active)': { + outline: '5px auto Highlight', + }, + }, +})); +``` + +An upgrade over `sx` in this case, since it allows the style to be reused between component instances. However, we run into the same issue as before: consumers would need to wrap their components, instead of us fixing them. + +#### global CSS / `GlobalStyles` + +```jsx + +``` + +Same issue as before: userland fix. Also, it's purely selector-based, and might be quite brittle due to class names and specificity. + +#### Theme `components.styleOverrides` + +```jsx +const theme = createTheme({ + components: { + MuiMenuItem: { + styleOverrides: { + root: { + [`&.${menuItemClasses.selected}`]: { + '@media (forced-colors: active)': { + forcedColorAdjust: 'none', + color: 'SelectedItemText', + backgroundColor: 'SelectedItem', + }, + }, + [`&.${menuItemClasses.focusVisible}, &:hover`]: { + '@media (forced-colors: active)': { + forcedColorAdjust: 'none', + color: 'HighlightText', + backgroundColor: 'Highlight', + }, + }, + }, + }, + }, + }, +}); +``` + +This might be the best way to address the issue by the users, since it's component-aware and central. If a consumer would have chosen to address this issue in their Material UI based solution, this would have probably the best solution. + +#### Built-in styles inside components + +This is the most straightforward approach to address the issue from our end: actually improving the component styles. + +```js +const ButtonRoot = styled(ButtonBase)(({ theme }) => ({ + // regular Button styles... + + '@media (forced-colors: active)': { + '&:focus-visible': { + outline: '5px auto Highlight', + }, + + [`&.${buttonClasses.disabled}`]: { + color: 'GrayText', + opacity: 1, + }, + }, +})); +``` + +It involves us going through all our components, identify the gaps, and address them, merge the changes and release. Solved. However, some of our consumers most probably addressed these issues themselves, and our changes might become breaking changes for them. We still wanted to address the issue on our end, but without any potential issues on update. + +#### Theme Enhancer + +To avoid breaking changes and also deliver a fix from our side, we considered to deliver a theme enhancer, which would be a fully opt-in solution. The API proposed is quite simple: + +```js +const userTheme = createTheme({ + // user theme options +}); +const enhancedTheme = enhance(userTheme, { + // system color value overrides +}); +``` + +This gives us control over the user's theme in order to provide the required style overrides, while also giving the user the choice of using it. + +### The solution + +```js +import { createTheme, enhanceHighContrast } from '@mui/material/styles'; + +const theme = enhanceHighContrast(createTheme(), { + activeBackground: 'SelectedItem', + activeText: 'SelectedItemText', +}); +``` + +Finally, we decided to go ahead with the [theme enhancer](https://mui.com/material-ui/customization/palette/#windows-high-contrast-mode). The [enhanceHighContrast](https://mui.com/material-ui/customization/theming/#enhancehighcontrast-theme-tokens-theme) function takes the user's theme as argument, an object with token overrides, and adds the fixes on top of the theme. + +It involves minimum changes from the user, it's completely opt-in and addresses all issues through the theme object's `styleOverrides`. Of course, we would strongly encourage everyone to use it, whether or not they have addressed the issue in-house, as there might still be gaps. + +For example, here's the theme overrides fix for the FormLabel's error and disabled states, using tokens: + +```js +MuiFormLabel: { + ...c?.MuiFormLabel, + styleOverrides: { + ...c?.MuiFormLabel?.styleOverrides, + root: [ + c?.MuiFormLabel?.styleOverrides?.root, + { + [`&.${formLabelClasses.error}`]: { + [HCM]: { + color: hcTokens.error, + }, + }, + [`&.${formLabelClasses.disabled}`]: { + [HCM]: { + color: hcTokens.disabled, + }, + }, + }, + ], + }, +}, +``` + +## Takeaways + +The `enhanceHighContrast` feature is part of Material UI's effort to improve the library's accessibility and encourage accessible UX in web apps. We look forward to receiving feedback as we aim to improve our support for Windows High Contrast mode even further. diff --git a/docs/src/modules/components/TopLayoutBlog.js b/docs/src/modules/components/TopLayoutBlog.js index 464fb50109e838..49e4f75a17de8a 100644 --- a/docs/src/modules/components/TopLayoutBlog.js +++ b/docs/src/modules/components/TopLayoutBlog.js @@ -169,6 +169,11 @@ export const authors = { avatar: 'https://avatars.githubusercontent.com/u/12778398', github: 'bernardobelchior', }, + silviuaavram: { + name: 'Silviu Alexandru Avram', + avatar: 'https://avatars.githubusercontent.com/u/11275392', + github: 'silviuaavram', + }, }; const classes = { From 206b4e7ce0f19cc4f86bf454e81f667e95c09202 Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Thu, 2 Jul 2026 11:20:12 +0300 Subject: [PATCH 2/6] improve the documentation --- ...igning-for-forced-colors-in-material-ui.md | 98 +++++++++---------- 1 file changed, 44 insertions(+), 54 deletions(-) diff --git a/docs/pages/blog/designing-for-forced-colors-in-material-ui.md b/docs/pages/blog/designing-for-forced-colors-in-material-ui.md index 06591b74b20829..24ccbab3c68bcb 100644 --- a/docs/pages/blog/designing-for-forced-colors-in-material-ui.md +++ b/docs/pages/blog/designing-for-forced-colors-in-material-ui.md @@ -7,23 +7,27 @@ tags: ['Material UI', 'Accessibility', 'Guide'] manualCard: false --- +This post walks through how Windows High Contrast mode maps to the CSS `forced-colors` model, what gaps we found in Material UI components, which implementation paths we considered, and why we chose an opt-in theme enhancer. + ## Windows High Contrast mode -As described in the [Microsoft blog](https://blogs.windows.com/msedgedev/2020/09/17/styling-for-windows-high-contrast-with-new-standards-for-forced-colors/), the Windows High Contrast mode is an accessibility feature that is designed to increase text legibility and improve readability. It can be enabled at an Operating System level, by going to Settings > Ease of Access > High contrast, then 'Turn on high contrast'. +As described in the [Microsoft blog](https://blogs.windows.com/msedgedev/2020/09/17/styling-for-windows-high-contrast-with-new-standards-for-forced-colors/), Windows High Contrast mode is an accessibility feature designed to increase text legibility and improve readability. In modern Windows versions, users configure this through High Contrast or Contrast themes, depending on the version. -The user can select system colors which are going to be applied, via the user theme, to their UI. In turn, web developers need to make sure these system colors are applied correctly to their apps, such that the resulting experience is correct. +The user can select a limited palette of system colors that the operating system applies across the UI. Web developers need to make sure their apps integrate with those colors instead of fighting them. ## CSS System Colors -When Windows High Contrast Mode (WHCM) is turned on, complex color gradients, shadows, and background images are removed. Text is converted to a solid color that contrasts with the background. Buttons, borders, and menus are reduced to simple outlines and solid fills for clear visibility. +When Windows High Contrast mode is turned on, browsers expose it to web content through the `forced-colors` media feature. In this mode, complex color gradients, shadows, and some background images are removed or simplified. Text, borders, backgrounds, controls, and selections are mapped to a small set of colors that are expected to contrast with each other. + +These overrides are expressed with [CSS system colors](https://www.w3.org/TR/css-color-4/#css-system-colors). They have browser and operating system defaults, but they can also reflect the user's chosen contrast theme. For example, `SelectedItem` and `SelectedItemText` represent the background and text colors for selected items. `Canvas` and `CanvasText` represent the page background and regular text. `GrayText` represents disabled content. -These overrides are done using the [css system colors](https://www.w3.org/TR/css-color-4/#css-system-colors). They have OS defaults, but, as mentioned above, these could be changed by the user as well. System colors are meant to cover all cases where the OS needs a contrasting alternative to the colors it will replace. For example, `SelectedItem` and `SelectedItemText` are used to override a selected menu item's background and text colors. When doing an value override, the user should make sure that these colors contrast at least to the WCAG AA standard, but this part is out of scope for this article. Instead, we are going to focus on using these system colors in our components, to make sure the app in WHCM looks as expected. +Browsers force a defined set of color-related properties at paint time. That detail matters: the CSS cascade can still contain author colors, but the browser paints a forced system color instead. System color keywords give authors a way to make the rest of the component fit that same palette. ## CSS Features -CSS provides a media query that detects whenever WHCM is active: +CSS provides a media query that detects whenever forced colors are active: -### @media(forced-colors: $value) +### @media (forced-colors: $value) ```css @media (forced-colors: active) { @@ -34,53 +38,35 @@ CSS provides a media query that detects whenever WHCM is active: } ``` -Here, we are using the a duo of system colors to style a menu item's selected state. For native HTML elements, the browser will do that out of the box, so it's important that we do it ourselves for the custom built elements. +Here, we're using a pair of system colors to style a menu item's selected state. Native HTML elements get a lot of this behavior from the browser for free. Custom-built components do not always have that same semantic mapping, so component libraries need to fill the gap. ### forced-color-adjust: $value -Another CSS feature that is useful here is the [forced-color-adjust](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/forced-color-adjust) property. This is an escape hatch that should be used whenever we don't want the OS to apply WHCM styles on an elements and its children. +Another useful CSS feature is the [forced-color-adjust](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/forced-color-adjust) property. It can opt an element out of automatic forced-color adjustment. -In the example above, the styles are probably not going to be enough, as the OS will also apply a highlight style over the menu item's text, and that could look weird. Consequently, we can use the `forced-color-adjust: none` escape hatch to get rid of it. Just keep in mind that you are now in full control and need to make sure that the styles chosen for WHCM work well. +This is a sharp tool. When `forced-color-adjust: none` is used, the browser stops protecting that element's colors, so the author becomes responsible for the contrast of the element and its children. In Material UI, that made sense for selected and active states where we wanted to pair colors like `SelectedItem` with `SelectedItemText` or `Highlight` with `HighlightText`. -## Chromium Flags +## Chrome DevTools Emulation -Some useful Chromium features that allow developers to emulate and test WHCM are found in the DevTools' Rendering tab. These are: +Some useful Chrome DevTools features for testing forced colors are found in the Rendering tab: -- `forced-colors`: when set to `active`, it emulates the WHCM inside the browser. -- `prefer-color-scheme`: force either `light` or `dark` mode, which is also useful for WHCM, since it's supposed to work in both color schemes. +- `forced-colors`: when set to `active`, it emulates forced colors inside the browser. +- `prefers-color-scheme`: forces either `light` or `dark`, which is useful because forced colors should work in both color schemes. -## WHCM with Material UI Components +## Forced Colors with Material UI Components ### The problem -Material UI components generally behaved well in WHCM, but there were some corner cases that we missed. - -The fixes cover: - -- AccordionSummary disabled opacity. -- Autocomplete disabled options. -- Autocomplete focused and selected options. -- Checkbox disabled color. -- FilledInput error and disabled states. -- FormControlLabel disabled label. -- FormHelperText error and disabled states. -- FormLabel error and disabled states. -- Input error and disabled states. -- InputBase placeholder opacity. -- LinearProgress root, bar, and buffer visibility. -- MenuItem disabled, hover, focus-visible, and selected states. -- ListItemIcon inherited color. -- ListItemButton hover, focus-visible, and selected states. -- NativeSelect disabled icon. -- OutlinedInput error and disabled states. -- Radio disabled color. -- Slider disabled track and thumb. -- Switch disabled track and thumb. -- ButtonBase focus outline. -- Tooltip visible border. -- ToggleButton selected and selected hover states. - -If these cases would not have been properly visible within our default themes, it would have been a huge issue. Consequently, it was critical for us to address the gaps in WHCM. Lucky for us, Material UI provides many options to style components, so the real challenge was to choose the best one for the job. +Material UI components generally behaved well in forced colors, but there were some corner cases that we missed. + +The fixes fall into a few groups: + +- Form states: `FilledInput`, `Input`, `OutlinedInput`, `FormLabel`, `FormHelperText`, `FormControlLabel`, placeholders, and disabled native select icons. +- Selection and navigation states: `Autocomplete`, `MenuItem`, `ListItemButton`, and `ListItemIcon`. +- Disabled controls: `Checkbox`, `Radio`, `Slider`, and `Switch`. +- Feedback, focus, and overlays: `LinearProgress`, `ButtonBase`, `Tooltip`, `AccordionSummary`, and `ToggleButton`. + +If these cases are not visible in forced colors, users can lose access to state: disabled, selected, focused, invalid, or in progress. The tricky part was not finding a way to style them. Material UI has many styling entry points. The real challenge was choosing the right level of the system. The following demo shows these affected states side by side. In Chrome, open DevTools > More tools > Rendering, then set `forced-colors` to `active` and `prefers-color-scheme` to `light` or `dark`. @@ -90,6 +76,8 @@ You can also use your operating system's high contrast or contrast theme setting ### The options +We considered the options along three dimensions: scope, user effort, and update risk. + #### The `sx` prop ```jsx @@ -105,7 +93,7 @@ You can also use your operating system's high contrast or contrast theme setting ``` -The quickest way to patch a styling issue. However, we needed to fix the components on our end, instead of asking our consumers to patch all their components via `sx`. +This is the quickest way to patch a local styling issue. It's great for app-specific edge cases, but it would push the responsibility to every Material UI user and every component instance. That is not enough for a library-level accessibility fix. #### The `styled()` wrappers @@ -122,9 +110,9 @@ const AppButton = styled(Button)(() => ({ })); ``` -An upgrade over `sx` in this case, since it allows the style to be reused between component instances. However, we run into the same issue as before: consumers would need to wrap their components, instead of us fixing them. +This is a step up from `sx` because the fix can be reused between component instances. But it still creates a userland wrapper that app teams need to adopt everywhere. It also fragments the fix across applications instead of solving it in Material UI. -#### global CSS / `GlobalStyles` +#### Global CSS / `GlobalStyles` ```jsx ``` -Same issue as before: userland fix. Also, it's purely selector-based, and might be quite brittle due to class names and specificity. +Global CSS can cover a broad surface area, but it is selector-based. That makes it more brittle around class names, slots, specificity, and composition. It can be useful as a temporary app-level patch, but it is not the most maintainable way for Material UI to encode component-aware state fixes. #### Theme `components.styleOverrides` @@ -172,11 +160,11 @@ const theme = createTheme({ }); ``` -This might be the best way to address the issue by the users, since it's component-aware and central. If a consumer would have chosen to address this issue in their Material UI based solution, this would have probably the best solution. +For application teams, this is probably the best centralized option. It is component-aware, lives in the theme, and avoids repeated `sx` or wrapper code. It also matches the mechanism Material UI already uses for component customization. #### Built-in styles inside components -This is the most straightforward approach to address the issue from our end: actually improving the component styles. +From the library side, the most direct option would be to put the forced-colors styles inside each component. ```js const ButtonRoot = styled(ButtonBase)(({ theme }) => ({ @@ -195,11 +183,11 @@ const ButtonRoot = styled(ButtonBase)(({ theme }) => ({ })); ``` -It involves us going through all our components, identify the gaps, and address them, merge the changes and release. Solved. However, some of our consumers most probably addressed these issues themselves, and our changes might become breaking changes for them. We still wanted to address the issue on our end, but without any potential issues on update. +This would make the fix automatic, which is attractive for accessibility. But there is a trade-off: some users have already patched these states in their own themes. Shipping built-in styles could change their UI on upgrade, even if the change is well intentioned. We wanted to provide the fix from our side without surprising existing applications. #### Theme Enhancer -To avoid breaking changes and also deliver a fix from our side, we considered to deliver a theme enhancer, which would be a fully opt-in solution. The API proposed is quite simple: +To avoid breaking changes and still deliver a Material UI-owned fix, we considered a fully opt-in theme enhancer. The proposed API was intentionally small: ```js const userTheme = createTheme({ @@ -210,7 +198,7 @@ const enhancedTheme = enhance(userTheme, { }); ``` -This gives us control over the user's theme in order to provide the required style overrides, while also giving the user the choice of using it. +This gives Material UI a central place to provide the required `styleOverrides`, while giving users explicit control over when those overrides are added. ### The solution @@ -223,9 +211,9 @@ const theme = enhanceHighContrast(createTheme(), { }); ``` -Finally, we decided to go ahead with the [theme enhancer](https://mui.com/material-ui/customization/palette/#windows-high-contrast-mode). The [enhanceHighContrast](https://mui.com/material-ui/customization/theming/#enhancehighcontrast-theme-tokens-theme) function takes the user's theme as argument, an object with token overrides, and adds the fixes on top of the theme. +Finally, we decided to go ahead with the [theme enhancer](https://mui.com/material-ui/customization/palette/#windows-high-contrast-mode). The [enhanceHighContrast](https://mui.com/material-ui/customization/theming/#enhancehighcontrast-theme-tokens-theme) function takes the user's theme as its first argument, accepts optional system color token overrides, and layers forced-colors fixes on top of the theme. -It involves minimum changes from the user, it's completely opt-in and addresses all issues through the theme object's `styleOverrides`. Of course, we would strongly encourage everyone to use it, whether or not they have addressed the issue in-house, as there might still be gaps. +This approach requires minimal user code, remains completely opt-in, preserves existing `components.styleOverrides`, and keeps the forced-colors behavior centralized in the theme. It also gives teams a gradual path: they can adopt the enhancer, compare it with their existing customizations, and override individual system color tokens when needed. For example, here's the theme overrides fix for the FormLabel's error and disabled states, using tokens: @@ -255,4 +243,6 @@ MuiFormLabel: { ## Takeaways -The `enhanceHighContrast` feature is part of Material UI's effort to improve the library's accessibility and encourage accessible UX in web apps. We look forward to receiving feedback as we aim to improve our support for Windows High Contrast mode even further. +Forced colors are not just another dark mode. They are a user-controlled accessibility mode where the browser and operating system intentionally reduce the color palette. Supporting that mode well means respecting system colors, using `forced-color-adjust` carefully, and making component states visible without asking every app team to rediscover the same fixes. + +The `enhanceHighContrast` feature is part of Material UI's effort to improve accessibility and encourage accessible UX in web apps. We look forward to feedback as we continue improving support for Windows High Contrast mode and forced colors. From 22de89e8a86edc297781b3fe531c2dd13cebf94a Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Thu, 2 Jul 2026 13:26:59 +0300 Subject: [PATCH 3/6] fix space for Material UI --- ...igning-for-forced-colors-in-material-ui.md | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/pages/blog/designing-for-forced-colors-in-material-ui.md b/docs/pages/blog/designing-for-forced-colors-in-material-ui.md index 24ccbab3c68bcb..519448e5e8cacf 100644 --- a/docs/pages/blog/designing-for-forced-colors-in-material-ui.md +++ b/docs/pages/blog/designing-for-forced-colors-in-material-ui.md @@ -1,13 +1,13 @@ --- -title: Windows High Contrast mode support in Material UI -description: Learn how Windows High Contrast maps to CSS system colors and why Material UI uses an opt-in theme enhancer for forced-colors support. +title: Windows High Contrast mode support in Material UI +description: Learn how Windows High Contrast maps to CSS system colors and why Material UI uses an opt-in theme enhancer for forced-colors support. date: 2026-07-01T00:00:00.000Z authors: ['silviuaavram'] -tags: ['Material UI', 'Accessibility', 'Guide'] +tags: ['Material UI', 'Accessibility', 'Guide'] manualCard: false --- -This post walks through how Windows High Contrast mode maps to the CSS `forced-colors` model, what gaps we found in Material UI components, which implementation paths we considered, and why we chose an opt-in theme enhancer. +This post walks through how Windows High Contrast mode maps to the CSS `forced-colors` model, what gaps we found in Material UI components, which implementation paths we considered, and why we chose an opt-in theme enhancer. ## Windows High Contrast mode @@ -44,7 +44,7 @@ Here, we're using a pair of system colors to style a menu item's selected state. Another useful CSS feature is the [forced-color-adjust](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/forced-color-adjust) property. It can opt an element out of automatic forced-color adjustment. -This is a sharp tool. When `forced-color-adjust: none` is used, the browser stops protecting that element's colors, so the author becomes responsible for the contrast of the element and its children. In Material UI, that made sense for selected and active states where we wanted to pair colors like `SelectedItem` with `SelectedItemText` or `Highlight` with `HighlightText`. +This is a sharp tool. When `forced-color-adjust: none` is used, the browser stops protecting that element's colors, so the author becomes responsible for the contrast of the element and its children. In Material UI, that made sense for selected and active states where we wanted to pair colors like `SelectedItem` with `SelectedItemText` or `Highlight` with `HighlightText`. ## Chrome DevTools Emulation @@ -53,11 +53,11 @@ Some useful Chrome DevTools features for testing forced colors are found in the - `forced-colors`: when set to `active`, it emulates forced colors inside the browser. - `prefers-color-scheme`: forces either `light` or `dark`, which is useful because forced colors should work in both color schemes. -## Forced Colors with Material UI Components +## Forced Colors with Material UI Components ### The problem -Material UI components generally behaved well in forced colors, but there were some corner cases that we missed. +Material UI components generally behaved well in forced colors, but there were some corner cases that we missed. The fixes fall into a few groups: @@ -66,7 +66,7 @@ The fixes fall into a few groups: - Disabled controls: `Checkbox`, `Radio`, `Slider`, and `Switch`. - Feedback, focus, and overlays: `LinearProgress`, `ButtonBase`, `Tooltip`, `AccordionSummary`, and `ToggleButton`. -If these cases are not visible in forced colors, users can lose access to state: disabled, selected, focused, invalid, or in progress. The tricky part was not finding a way to style them. Material UI has many styling entry points. The real challenge was choosing the right level of the system. +If these cases are not visible in forced colors, users can lose access to state: disabled, selected, focused, invalid, or in progress. The tricky part was not finding a way to style them. Material UI has many styling entry points. The real challenge was choosing the right level of the system. The following demo shows these affected states side by side. In Chrome, open DevTools > More tools > Rendering, then set `forced-colors` to `active` and `prefers-color-scheme` to `light` or `dark`. @@ -93,7 +93,7 @@ We considered the options along three dimensions: scope, user effort, and update ``` -This is the quickest way to patch a local styling issue. It's great for app-specific edge cases, but it would push the responsibility to every Material UI user and every component instance. That is not enough for a library-level accessibility fix. +This is the quickest way to patch a local styling issue. It's great for app-specific edge cases, but it would push the responsibility to every Material UI user and every component instance. That is not enough for a library-level accessibility fix. #### The `styled()` wrappers @@ -110,7 +110,7 @@ const AppButton = styled(Button)(() => ({ })); ``` -This is a step up from `sx` because the fix can be reused between component instances. But it still creates a userland wrapper that app teams need to adopt everywhere. It also fragments the fix across applications instead of solving it in Material UI. +This is a step up from `sx` because the fix can be reused between component instances. But it still creates a userland wrapper that app teams need to adopt everywhere. It also fragments the fix across applications instead of solving it in Material UI. #### Global CSS / `GlobalStyles` @@ -129,7 +129,7 @@ This is a step up from `sx` because the fix can be reused between component inst /> ``` -Global CSS can cover a broad surface area, but it is selector-based. That makes it more brittle around class names, slots, specificity, and composition. It can be useful as a temporary app-level patch, but it is not the most maintainable way for Material UI to encode component-aware state fixes. +Global CSS can cover a broad surface area, but it is selector-based. That makes it more brittle around class names, slots, specificity, and composition. It can be useful as a temporary app-level patch, but it is not the most maintainable way for Material UI to encode component-aware state fixes. #### Theme `components.styleOverrides` @@ -160,7 +160,7 @@ const theme = createTheme({ }); ``` -For application teams, this is probably the best centralized option. It is component-aware, lives in the theme, and avoids repeated `sx` or wrapper code. It also matches the mechanism Material UI already uses for component customization. +For application teams, this is probably the best centralized option. It is component-aware, lives in the theme, and avoids repeated `sx` or wrapper code. It also matches the mechanism Material UI already uses for component customization. #### Built-in styles inside components @@ -187,7 +187,7 @@ This would make the fix automatic, which is attractive for accessibility. But th #### Theme Enhancer -To avoid breaking changes and still deliver a Material UI-owned fix, we considered a fully opt-in theme enhancer. The proposed API was intentionally small: +To avoid breaking changes and still deliver a Material UI-owned fix, we considered a fully opt-in theme enhancer. The proposed API was intentionally small: ```js const userTheme = createTheme({ @@ -198,7 +198,7 @@ const enhancedTheme = enhance(userTheme, { }); ``` -This gives Material UI a central place to provide the required `styleOverrides`, while giving users explicit control over when those overrides are added. +This gives Material UI a central place to provide the required `styleOverrides`, while giving users explicit control over when those overrides are added. ### The solution @@ -245,4 +245,4 @@ MuiFormLabel: { Forced colors are not just another dark mode. They are a user-controlled accessibility mode where the browser and operating system intentionally reduce the color palette. Supporting that mode well means respecting system colors, using `forced-color-adjust` carefully, and making component states visible without asking every app team to rediscover the same fixes. -The `enhanceHighContrast` feature is part of Material UI's effort to improve accessibility and encourage accessible UX in web apps. We look forward to feedback as we continue improving support for Windows High Contrast mode and forced colors. +The `enhanceHighContrast` feature is part of Material UI's effort to improve accessibility and encourage accessible UX in web apps. We look forward to feedback as we continue improving support for Windows High Contrast mode and forced colors. From 673e71d1c2be2554a63ade4863f4d2b2c00037ec Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Thu, 2 Jul 2026 13:27:50 +0300 Subject: [PATCH 4/6] sync demos --- .../data/material/customization/palette/HighContrastShowcase.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data/material/customization/palette/HighContrastShowcase.js b/docs/data/material/customization/palette/HighContrastShowcase.js index 0c9849d22c4fba..fadbc52309b43e 100644 --- a/docs/data/material/customization/palette/HighContrastShowcase.js +++ b/docs/data/material/customization/palette/HighContrastShowcase.js @@ -132,7 +132,7 @@ export default function HighContrastShowcase() { light or dark. Then toggle the enhancer to compare the fix. - + Date: Thu, 2 Jul 2026 15:08:26 +0300 Subject: [PATCH 5/6] fix tag --- docs/pages/blog/designing-for-forced-colors-in-material-ui.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pages/blog/designing-for-forced-colors-in-material-ui.md b/docs/pages/blog/designing-for-forced-colors-in-material-ui.md index 519448e5e8cacf..1254af31a21a0b 100644 --- a/docs/pages/blog/designing-for-forced-colors-in-material-ui.md +++ b/docs/pages/blog/designing-for-forced-colors-in-material-ui.md @@ -3,7 +3,7 @@ title: Windows High Contrast mode support in Material UI description: Learn how Windows High Contrast maps to CSS system colors and why Material UI uses an opt-in theme enhancer for forced-colors support. date: 2026-07-01T00:00:00.000Z authors: ['silviuaavram'] -tags: ['Material UI', 'Accessibility', 'Guide'] +tags: ['Material UI', 'Accessibility', 'Guide'] manualCard: false --- From 47a4625780b625539e81023c1f1b84ee16b14749 Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Thu, 2 Jul 2026 15:43:41 +0300 Subject: [PATCH 6/6] remove open control prop --- .../material/customization/palette/HighContrastShowcase.js | 3 +-- .../material/customization/palette/HighContrastShowcase.tsx | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/data/material/customization/palette/HighContrastShowcase.js b/docs/data/material/customization/palette/HighContrastShowcase.js index fadbc52309b43e..e568864df77330 100644 --- a/docs/data/material/customization/palette/HighContrastShowcase.js +++ b/docs/data/material/customization/palette/HighContrastShowcase.js @@ -294,7 +294,6 @@ export default function HighContrastShowcase() {
- Shows disabled and selected option styling. + Open the menu to compare the fixed disabled and selected option styles.
diff --git a/docs/data/material/customization/palette/HighContrastShowcase.tsx b/docs/data/material/customization/palette/HighContrastShowcase.tsx index 833cd35adc8dd4..d56d68802a3069 100644 --- a/docs/data/material/customization/palette/HighContrastShowcase.tsx +++ b/docs/data/material/customization/palette/HighContrastShowcase.tsx @@ -288,7 +288,6 @@ export default function HighContrastShowcase() {
- Shows disabled and selected option styling. + Open the menu to compare the fixed disabled and selected option styles.