Relative colors
Relative color syntax lets us take an existing colour, pull it apart into its individual channels, and rebuild it in CSS, with no preprocessor. The shape is always the same: <function>(from <origin-color> <channels> / <alpha>).
The from keyword accepts any colour — a hex value, a named colour, a custom property — and exposes its channels as keywords we can reference inside the function.
.rgb-red {
color: rgb(from #ff0000 r g b);
}
.rgb-red-50 {
color: rgb(from #ff0000 r g b / 0.5);
}
Here r, g and b resolve to the channel values of #ff0000 (255, 0, 0), so .rgb-red is just red again. Passing the channels straight through is a no-op. The interesting part is that we now have handles on them. The / 0.5 after the channels sets the alpha, which is how we get a translucent version of a colour we never had to hardcode.
The origin colour doesn't have to match the function we're using. Handing a hex value to hsl() converts it for us, and we get hue, saturation and lightness to work with instead:
.hsl-red {
color: hsl(from #ff0000 h s l);
}
.hsl-red-50 {
color: hsl(from #ff0000 h s l / 0.5);
}
We can replace any of the variables we've pulled with a new value, so, we can create lighter and darker versions of a base colour.
:root {
--base: hsl(217 73% 50%);
--base-light: hsl(from var(--base) h s 75%);
--base-dark: hsl(from var(--base) h s 25%);
}
Light and dark themes
light-dark() takes two colours and returns the first one when the current colour scheme resolves to light, the second when it resolves to dark.
:root {
/* follow the user preferences */
color-scheme: light dark;
--text-heading: light-dark(#000, #fff);
--text-body: light-dark(#212121, #efefef);
--surface: light-dark(#efefef, #212121);
}
/* if user picks a light theme */
html[data-theme="light"] {
color-scheme: light;
}
/* if user picks a dark theme */
html[data-theme="dark"] {
color-scheme: dark;
}
Declaring color-scheme: light dark says "this page supports both", and the browser picks based on the OS preference. The two data-theme rules let a user override that from a theme toggle: setting a single-value color-scheme pins the resolved scheme, and every light-dark() in the subtree switches with it. No media query duplication, and no second set of custom properties to keep in sync.