Using CSS Custom Properties to Build Composable Style Systems
I'm gonna try to distill this down into something approachable.
Let's say you want to style your buttons. You make a palette of colors, and use those colors to style your button:
[data-component='ia.input.button'] {
color: var(--palette-primary-50);
background-color: var(--palette-primary-500);
border-color: var(--palette-primary-600);
border-width: 1px;
border-style: solid;
border-radius: 5px;
}
:root {
--palette-primary-50: #edf5fd;
--palette-primary-100: #e3effb;
--palette-primary-200: #c7dff7;
--palette-primary-300: #97c3f0;
--palette-primary-400: #4393e4;
--palette-primary-500: #0b6bcb;
--palette-primary-600: #185ea5;
--palette-primary-700: #12467b;
--palette-primary-800: #0a2744;
--palette-primary-900: #051423;
--palette-neutral-50: #fbfcfe;
--palette-neutral-100: #f0f4f8;
--palette-neutral-200: #dde7ee;
--palette-neutral-300: #cdd7e1;
--palette-neutral-400: #9fa6ad;
--palette-neutral-500: #636b74;
--palette-neutral-600: #555e68;
--palette-neutral-700: #32383e;
--palette-neutral-800: #171a1c;
--palette-neutral-900: #0b0d0e;
--palette-danger-50: #fef6f6;
--palette-danger-100: #fce4e4;
--palette-danger-200: #f7c5c5;
--palette-danger-300: #f09898;
--palette-danger-400: #e47474;
--palette-danger-500: #c41c1c;
--palette-danger-600: #a51818;
--palette-danger-700: #7d1212;
--palette-danger-800: #430a0a;
--palette-danger-900: #240505;
}
Cool! But now all our buttons are blue, and we need more options. We have 3 color palettes, and it would be nice if we could use them all on our buttons.
Let's add a layer of indirection:
.psc-Color\/primary {
--palette-50: var(--palette-primary-50);
--palette-100: var(--palette-primary-100);
--palette-200: var(--palette-primary-200);
--palette-300: var(--palette-primary-300);
--palette-400: var(--palette-primary-400);
--palette-500: var(--palette-primary-500);
--palette-600: var(--palette-primary-600);
--palette-700: var(--palette-primary-700);
--palette-800: var(--palette-primary-800);
--palette-900: var(--palette-primary-900);
}
.psc-Color\/neutral {
--palette-50: var(--palette-neutral-50);
--palette-100: var(--palette-neutral-100);
--palette-200: var(--palette-neutral-200);
--palette-300: var(--palette-neutral-300);
--palette-400: var(--palette-neutral-400);
--palette-500: var(--palette-neutral-500);
--palette-600: var(--palette-neutral-600);
--palette-700: var(--palette-neutral-700);
--palette-800: var(--palette-neutral-800);
--palette-900: var(--palette-neutral-900);
}
.psc-Color\/danger {
--palette-50: var(--palette-danger-50);
--palette-100: var(--palette-danger-100);
--palette-200: var(--palette-danger-200);
--palette-300: var(--palette-danger-300);
--palette-400: var(--palette-danger-400);
--palette-500: var(--palette-danger-500);
--palette-600: var(--palette-danger-600);
--palette-700: var(--palette-danger-700);
--palette-800: var(--palette-danger-800);
--palette-900: var(--palette-danger-900);
}
And use this computed/indirect palette on our button:
[data-component='ia.input.button'] {
color: var(--palette-50);
background-color: var(--palette-500);
border-color: var(--palette-600);
border-width: 1px;
border-style: solid;
border-radius: 5px;
}
Now, by applying the style classes Color\neutral
, Color\primary
, and Color\danger
we can get three different looks for our button.
This is a simple example, but it can be taken much further.
Our pyro-mui-joy theme takes this to the extreme, with 4 variants (solid
, outlined
, soft
, and neutral
), 5 palettes (neutral
, primary
, success
, info
, and danger
), and three sizes (sm
, md
, and lg
) that can all be applied to a component to affect its look and feel.
When you get this scale though, handwriting the CSS classes becomes sadistic and you will want to turn to automation to generate them for you (maybe a topic for another time).