Hello, this is Chandler from Progate with the 17th day of the Progate Advent Calendar.
I work as a frontend engineer, and recently I've been getting acquainted with MUI v5, which is a UI component library. These kinds of libraries are great for keeping things consistent and also streamlining development but can come with limitations when customizing them to integrate with your own design system.
I've actually worked with MUI back a few years ago back when it was Material-UI v0.20.2, and I have to say it's come a long way. Back then there wasn't really a way to customize the components yourself as inline styling can't reach the inner elements, and I remember there was a lot of abusing the CSS !important
property. With the new MUI, though, component customization has become a lot more accessible. You can use the sx
prop or styled-components
to pass CSS to components on an individual level, and you can even override component styles on a global scale in the theme as documented here.
While components have become very customizable, I think there are still some restrictions with general theming. One problem I have run into is with the palette. The palette is pretty customizable as you can change the default colors and add new ones, but there's still more that I wish I could do with it. Each color has four variants main
, light
, dark
, and contrastText
- the dark
and light
are tones calculated based on main
using tonalOffset
. But what if I want to add more tones?
For example, if I want to add extraLight
to the primary
color, I can do something like this:
let theme = createTheme({ palette: { primary: { main: blue[700], extraLight: blue[300], }, ...})
But, there are two things I don't like about this - First, I have to redefine the main
for primary
. That isn't too big of a deal though as it would only have to be done for the default colors you want to keep. And second, I would have to define extraLight
for each color, even though this is something I would like to be calculated automatically for me with tonalOffset
I think it would be nice if on the palette object, I could do something like this:
palette: { tonalOffset: 0.2, tones: { light: { light: 1, // tonalOffset * 1 → 0.2 lighter than main extraLight: 2, // tonalOffset * 2 → 0.4 lighter than main }, dark: { dark: 1, // tonalOffset * 1 → 0.2 darker than main extraDark: 2, // tonalOffset * 2 → 0.4 darker than main } } }
The tones have keys for light
and darken
to use lighten
and darken
, respectively, and the keys within them refer to the name of the tone. The values are the degree of shift to use with tonalOffset for calculating the color.
However, based on the documentation, it doesn't look like there's a way to do something like this with the capabilities of the current palette settings.
Taking a look under the hood at how the dark
and light
tones are generated, I found that they are generated here with addLightAndDark
within augmentColor
.
It also looks like while augmentColor
may be accessible on the palette object in the theme, you can't adjust it or set it yourself.
While I can't get the extraLight
tone automatically generated using the existing settings, one thing I can do is generate the tones myself and merge it with my theme. Basically, I can create an incomplete theme object that only contains the palette with all of the extraLight
tones and then merge this with my theme.
const generateExtraLightTones = (theme) => { const paletteObject = {} for (const paletteKey of Object.keys(theme.palette)) { const main = theme.palette[paletteKey]['main'] if (!main) {continue} const extraLight = lighten(main, theme.palette.tonalOffset * 2) paletteObject[paletteKey] = {} paletteObject[paletteKey].extraLight = extraLight } return {palette: paletteObject} }
This will go through all the keys in the palette object and calculate the extraLight
tones based on the main color. As I said above, I want to calculate this based on the tonalOffset
, so I also grab that from the existing palette and use lighten
to calculate extraLight
.
Not all values in the palette object are color objects, so I need a way to make sure I'm only adding extraLight
to actual colors. Since only color objects will have a main
key, I simply check for this, but of course, this could be checked more strictly with the other object keys to make sure that only the colors are being grabbed.
Just for a clear image, this will create something like this:
{ palette: { primary: { extraLight: rgb(175, 119, 175), }, ... } }
And then it can be simply merged with the theme like this:
createTheme(theme, generateExtraLightTones(theme))
Now I can have all the extraLight
tones calculated for me.
This solves the current issue with extraLight
, but it would be nicer to consider the possibility of having more tones, such as extraDark
. For that, I could create something similar to the settings I originally hoped I could pass to the palette. This would be an object with the tones, separated by if they use lighten
or darken
, and refer to the shift degree with tonalOffset
.
const extraPaletteTones = { light: { extraLight: 2, // tonalOffset * 2 }, dark: { extraDark: 2, // tonalOffset * 2 } }
Then instead of calculating just the extraLight
tone, I can calculate the tones set in extraPaletteTones
.
const generateExtraPaletteTones = (theme) => { const paletteObject = {} for (const paletteKey of Object.keys(theme.palette)) { const main = theme.palette[paletteKey]['main'] if (!main) {continue} paletteObject[paletteKey] = {} for (const lightTone of Object.keys(extraTones.light)) { const lightToneShiftValue = extraTones.light[lightTone] paletteObject[paletteKey][lightTone] = lighten(main, lightToneShiftValue * theme.palette.tonalOffset) } for (const darkTone of Object.keys(extraTones.dark)) { const darkToneShiftValue = extraTones.dark[darkTone] paletteObject[paletteKey][darkTone] = darken(main, darkToneShiftValue * theme.palette.tonalOffset) } } return {palette: paletteObject} }
And that's pretty much it. Now I can add more tones types if I want and they'll be automatically calculated and set rather than having to set them individually myself.
Even though I've come across roadblocks like this, all in all, it has been really smooth using MUI so far. I'm looking forward to developing more with it and learning more.