Skip to content

Conversation

@siriwatknp
Copy link
Member

@siriwatknp siriwatknp commented Sep 30, 2024

Deploy preview

closes #40104
closes #36007
closes #37901
closes #43540
closes #43324
closes #43180
closes #43382
closes #41729
closes #41728

For detailed proposal, check out #43943

For Reviewer

Core Theme System Changes

  1. createPalette.js accept internal colorSpace to mix light, dark, and contrastText with pure CSS.
  2. createThemeNoVars attach color functions to the theme object.
  3. createThemeWithVars.js support a new flag nativeColor through CSS theme variables feature. (internal color space is set to OKLCH for perceptual uniformity)

Component Migrations (30+ components)

All components now use theme.alpha(), theme.lighten() and theme.darken() instead of direct alpha imports for backward compat.

The changes are ensured by the visual regression tests.

Documentation & Interactive Demos

  1. Main Documentation Page
  2. Upgrade guide

Codemod Implementation

npx @mui/codemod@latest v7.0.0/theme-color-functions <path>

Replace the usage of the alpha, lighten, and darken functions from @mui/system/colorManipulator to use the theme object instead.

- import { alpha, lighten, darken } from '@mui/system/colorManipulator';

- alpha(theme.palette.primary.main, 0.8)
+ theme.alpha((theme.vars || theme).palette.primary.main, 0.8)

- lighten(theme.palette.primary.main, 0.1)
+ theme.lighten(theme.palette.primary.main, 0.1)

- darken(theme.palette.primary.main, 0.3)
+ theme.darken(theme.palette.primary.main, 0.3)

Key Technical Decisions

  1. Why OKLCH as Default Color Space: Provides perceptually uniform color adjustments compared to RGB/HSL
  2. Why Relative Color Syntax: Enables pure CSS contrast calculations without JavaScript runtime overhead
  3. Why color-mix() for Lighten/Darken: More accurate color blending than mathematical transformations
  4. Backward Compatibility: All changes are opt-in via nativeColorSyntax flag, ensuring zero breaking changes

Overview

This PR introduces native CSS color manipulation support through CSS relative colors and color-mix(), eliminating JavaScript-based color calculations when the feature is enabled. The implementation leverages modern CSS color spaces, with OKLCH as the default for perceptually uniform color manipulations, while maintaining full backward compatibility.

What's Changed

  • ✨ Added nativeColorSyntax flag to enable native CSS color features
  • 🎨 Added theme.alpha(), theme.lighten(), and theme.darken() adapter functions for backward compatability.
  • 🔄 Migrated 30+ Material-UI components from using JS color manipulation to theme color functions.
  • 📚 Added comprehensive documentation page and interactive demos
  • 🛠️ Created codemod for automated migration of custom code
  • 🚀 Zero breaking changes - fully opt-in feature

Key Features

🎨 Native CSS Color Manipulation

When nativeColorSyntax is enabled:

  • OKLCH color space: Automatically selected for relative color calculations
  • CSS color-mix(): Hardware-accelerated color blending
  • Alias support: Reference external CSS variables as color values

🔧 Theme Color Manipulators

theme.alpha(color, coefficient)

The new alpha function uses relative color to adjust the alpha channel value:

// Standard mode (no CSS variables, no feature)
theme.alpha('#1976d2', 0.5); // → "rgba(25, 118, 210, 0.5)"

// ------------------------------

// When nativeColorSyntax is enabled, the color space is automatically set to `'oklch'` internally

theme.alpha(theme.vars.palette.primary.main, 0.5); // → "oklch(from var(--mui-palette-primary-main) l c h / 0.5)"
theme.alpha(theme.vars.palette.primary.main, '0.5 + 0.3'); // → "oklch(from var(--mui-palette-primary-main) l c h / calc(0.5 + 0.3))"
theme.alpha('oklch(0.65 0.3 28.95)', 0.5); // → "oklch(from oklch(0.65 0.3 28.95) l c h / 0.5)"

theme.lighten(color, coefficient) & theme.darken(color, coefficient)

These functions use CSS color-mix() when native color syntax is enabled:

// Standard mode - traditional color calculations
theme.lighten('#1976d2', 0.5); // → Traditional lighten calculation result
theme.darken('#1976d2', 0.3); // → Traditional darken calculation result

// ------------------------------

// When nativeColorSyntax is enabled, the color space is automatically set to `'oklch'` internally

theme.lighten('oklch(0.65 0.3 28.95)', 0.5); // → "color-mix(in oklch, oklch(0.65 0.3 28.95), #fff 50%)"
theme.darken('oklch(0.65 0.3 28.95)', 0.3); // → "color-mix(in oklch, oklch(0.65 0.3 28.95), #000 30%)"
theme.lighten(theme.vars.palette.primary.main, 0.3); // → "color-mix(in oklch, oklch(from var(--mui-palette-primary-main) l c h), #fff 30%)"

🌈 Cross-Color Space Compatibility

https://deploy-preview-43942--material-ui.netlify.app/material-ui/customization/css-theme-variables/native-color-syntax/#modern-color-spaces

↖️ Alias external token

https://deploy-preview-43942--material-ui.netlify.app/material-ui/customization/css-theme-variables/native-color-syntax/#aliasing-color-variables

✨ Automatic contrast calculation

Use relative color based on the color-contrast research.

https://deploy-preview-43942--material-ui.netlify.app/material-ui/customization/css-theme-variables/native-color-syntax/#contrast-color-function

Migration Strategy

Added an upgrade guide for users who want to start using native color syntax https://deploy-preview-43942--material-ui.netlify.app/material-ui/migration/upgrade-to-native-color-syntax/

Performance

From a quick benchmark using the blog template comparing between JS color manipulation and Native color syntax, there is no significant difference in terms of runtime performance.


@mui-bot
Copy link

mui-bot commented Sep 30, 2024

Netlify deploy preview

Bundle size report

Bundle Parsed Size Gzip Size
@mui/material ▼-1.67KB(-0.32%) 🔺+368B(+0.24%)
@mui/lab 🔺+1.7KB(+1.28%) 🔺+657B(+1.62%)
@mui/system 🔺+203B(+0.29%) 🔺+87B(+0.35%)
@mui/utils 0B(0.00%) 0B(0.00%)

Details of bundle changes

Generated by 🚫 dangerJS against 5f0450e

@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Oct 9, 2024
@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Oct 21, 2024
@github-actions github-actions bot added PR: out-of-date The pull request has merge conflicts and can't be merged. and removed PR: out-of-date The pull request has merge conflicts and can't be merged. labels Nov 14, 2024
@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jan 13, 2025
@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Apr 28, 2025
@siriwatknp
Copy link
Member Author

  • What about nativeColor

Sounds good to me.

I think it would be good to add a visual example of the differences so users can make a better decision. Otherwise they won't know how different the colors are.

I'll find some color examples

Besides that, there are two minor bugs on dark mode on the docs:

The first one looks correct to me, the colors are static. It should not change.
The second one is fixed.

Co-authored-by: mapache-salvaje <71297412+mapache-salvaje@users.noreply.github.com>
Signed-off-by: Siriwat K <siriwatkunaporn@gmail.com>
@DiegoAndai
Copy link
Member

The first one looks correct to me

I was referencing the text to the side of the boxes, it's unreadable 😅

Screenshot 2025-07-23 at 12 09 53

@siriwatknp
Copy link
Member Author

I was referencing the text to the side of the boxes, it's unreadable 😅

Nice catch! fixed.

@siriwatknp
Copy link
Member Author

siriwatknp commented Jul 28, 2025

@DiegoAndai
Copy link
Member

Thanks @siriwatknp. I think we should use the primary color for the default color shown:

Screenshot 2025-07-28 at 15 18 31

@siriwatknp siriwatknp requested a review from DiegoAndai July 29, 2025 02:37
@siriwatknp
Copy link
Member Author

Thanks @siriwatknp. I think we should use the primary color for the default color shown:

The primary color has no different. I think the point of the demo is to show a color that produces different result at first glance. For the default colors of Material UI, there is no difference. That's why I put this information in the caveat.

@DiegoAndai
Copy link
Member

The primary color has no different.

I'm not sure we should optimize for showing the most changed color either. The one we have now has different text color, which sure, showcases the differences, but most colors are very similar. In the end, we want to reassure users that even though there's a difference, it's minimal. Picking the color with most difference is not a good strategy for that message.

@siriwatknp
Copy link
Member Author

siriwatknp commented Jul 31, 2025

I'm not sure we should optimize for showing the most changed color either. The one we have now has different text color, which sure, showcases the differences, but most colors are very similar. In the end, we want to reassure users that even though there's a difference, it's minimal. Picking the color with most difference is not a good strategy for that message.

Then, should I remove the demo instead and proceed with the PR? It's minor so might be better to wait until users start using this feature.

@siriwatknp
Copy link
Member Author

Then, should I remove the demo instead and proceed with the PR? It's minor so might be better to wait until users start using this feature.

@mapache-salvaje Any thought about the caveat demo?

Copy link
Member

@DiegoAndai DiegoAndai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@siriwatknp I agree, the demo is not important enough to block the feature. Good to go on my side.

@siriwatknp siriwatknp merged commit 83bfdd7 into mui:master Aug 4, 2025
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment