A few years ago, CSS authors longed for a way to take a solid color (hex, named, HSL, whatever) and derive a version of it with arbitrary transparency—on the fly, without precomputing dozens of variants. One workaround was SASS/SCSS functions that split colors into channels and recompose them with an alpha. Others used color-mix()
as a clever fallback.
Then “relative color syntax” landed (in new CSS Color Module drafts and implementations) as a way to destructure an existing color into its channels and manipulate them in pure CSS.
What I’m proposing is: wrap that relative color syntax into a reusable CSS function-like abstraction (e.g. --opacity(color, alpha)
) so your code stays clean and expressive. You get the dynamism of SCSS but with pure, future-native CSS.
Below I explain how relative color syntax works for opacity transforms, how to wrap it cleanly, and how to use it safely today (with fallbacks). I’ll also embed a hands-on example you can play with on CodePen.
Understanding relative color syntax (for opacity)
Relative color syntax lets you say “take this origin color, break it into channels, then recombine (possibly altering channels or alpha) in your target color space.”
In practice, for opacity in RGB, you write:
rgb(from <origin-color> r g b / <alpha>)
from <origin-color>
tells the browser to parse origin into R, G, B (and alpha).r g b
means “use the same red, green, blue channel values as origin.”/ <alpha>
sets a new alpha (0–1 or 0%–100%).
So if your origin is #0ea5e9
(Tailwind’s sky-500
), you can do:
background: rgb(from #0ea5e9 r g b / 50%);
That yields the same color, but with 50% opacity.
If your origin is a CSS variable:
--my-color: #0ea5e9;
.my-element {
background: rgb(from var(--my-color) r g b / 30%);
}
That works consistently, no need to manually split channels.
You can also use other color spaces (HSL, OKLCH, etc.) and manipulate channels: but for simple opacity, RGB is the most direct.
Browser support: As of 2025, relative color syntax is broadly supported (e.g. Firefox 128, and others) so it’s becoming viable in production.
Still, fallback remains wise for older browsers.
Wrapping into a CSS “function”
Here’s how to package that pattern into a reusable macro-like abstraction. (Note: CSS doesn’t yet support user-defined functions in the same way as Sass, so we simulate one.)
@custom-media --any; /* just a placeholder if needed */
:root {
/* define a “function” as a custom property */
--opacity: "rgb(from var(--color) r g b / var(--alpha))";
}
/* A helper to “call” it */
@layer helpers {
.with-opacity {
/* set local variables for the “arguments” */
--color: red;
--alpha: 80%;
/* then use the stored “function” expression via var() and string substitution */
background-color: var(--opacity);
}
}
That basic idea isn’t perfect (string substitution is limited). A more robust variant:
:root {
/* no quotes—store only the pattern */
--fn-opacity-color: var; /* placeholder for pattern */
}
/* mixin-like using `@property` (future) or just inline usage */
.my-card {
--base: skyblue;
--target-alpha: 20%;
border-color: rgb(from var(--base) r g b / var(--target-alpha));
}
If CSS later allows real user functions, you could imagine:
@function opacity(color, alpha) {
result: rgb(from color r g b / alpha);
}
.card {
border-color: opacity(var(--color-secondary), 80%);
}
For now, inline use is straightforward and clear.
A live example
Here’s a full demo you can explore on CodePen:
See the Pen Untitled by Alex Ivanovs (@stackdiary) on CodePen.
You should see boxes tinted with the same hue but varying transparency. You can tweak the / 50%
or / 20%
inline and see instant effect.
Fallback strategy and caveats
Because some browsers or older engines may not support relative color syntax (or only with experimental flags), you may want a fallback.
One common fallback is color-mix()
, which mixes your color with transparent
:
/* fallback first */
.element {
background: color-mix(in srgb, var(--brandColor) 50%, transparent);
}
/* preferred */
@supports (color: rgb(from var(--brandColor) r g b / 50%)) {
.element {
background: rgb(from var(--brandColor) r g b / 50%);
}
}
This ensures browsers without support still get a semi-transparent version.
Also: using relative colors means the origin color is converted into the target color space internally. Be mindful when mixing color spaces (e.g. origin in HSL, target in RGB) — conversion steps may introduce rounding or gamut limitations.
Finally: don’t store the “function” as a literal string unless you have a CSS processing step; most CSS engines don’t support generic string evaluation. The pattern is best used inline or in a small abstraction layer you control.
Next steps and takeaways
Use rgb(from var(--color) r g b / <alpha>)
(or analogous syntax in other color spaces) to derive transparent variants on the fly. Wrap that into a naming convention or small helper layer, but avoid over-engineering abstractions until CSS user functions mature.
Always include a fallback (e.g. color-mix()
via @supports
) for older browsers.