This note has an interactive demo. Try it in the original
CSS finally has a function that does exactly one thing: take a color and change how transparent it is. alpha(from var(--brand) / 40%) is your brand color at 40% opacity, whatever format --brand was written in. It works in Chrome 152, Firefox 155 and Safari 27, and it has been Baseline since September 14, 2026. In the demo above, a single variable feeds a glass card, a progress bar, the borders and a six-step scale. Pick a color, drag the slider, and the whole thing follows.
Why this needed its own function
If you keep a design system with tokens, you know the problem. You define --brand once, and then you need the same color at 10% for a background, 30% for a border, 50% for a glow. For years the options were keeping a separate hand-written rgba() for each one (and forgetting to update them when the brand changes), or reaching for heavier tools:
-
Relative color syntax (
rgb(from var(--brand) r g b / 40%)) works, but you have to choose a color function and spell out every channel. Withrgb(),hsl()orhwb()the output is serialized ascolor(srgb …), according to MDN. -
color-mix()withtransparent(color-mix(in oklab, var(--brand) 40%, transparent)) also works, but now you're picking an interpolation space just to make something see-through.
alpha() skips both decisions. According to the spec, it refers to an origin color and "only change[s] the alpha channel". The color components stay as they are, and "the result of this function is in the color space of the origin color".
How it works
The grammar is short. This is the one line in CSS Color Module Level 5:
alpha() = alpha(from <color> / [<alpha-value> | none])
Two things to know:
- After
fromgoes any color: a hex value,oklch(), a named color, orvar(--brand). - After the slash goes the new alpha. Inside it, the keyword
alphameans the origin color's current alpha as a number (1.0 is 100%), so you can work relative to it.
This is the pattern the demo uses, minus the slider plumbing:
:root {
--brand: oklch(62% 0.24 300);
}
.glass {
/* same color, fixed opacity */
background: alpha(from var(--brand) / 16%);
border: 1px solid alpha(from var(--brand) / 57%);
box-shadow: 0 18px 50px -18px alpha(from var(--brand) / 47%);
}
.ghost {
/* half of whatever alpha the origin already had */
background: alpha(from var(--brand) / calc(alpha * 0.5));
}
Unlike oklch(from …) or rgb(from …), alpha() doesn't expose the channel keywords (l, c, h, r, g, b). MDN is explicit that it's "solely concerned with the alpha channel". Use it when that's all you want to change, and keep relative color syntax for when you want a lighter or more saturated variant.
Seeing the color space difference
The bottom panel of the demo runs getComputedStyle() on the same color written three ways. In Chromium 153, with oklch(62% 0.24 300) as the origin and 42% opacity, I get:
alpha(from … / 42%) → oklch(0.62 0.24 300 / 0.42)
rgb(from … r g b / 42%) → color(srgb 0.626103 0.325981 0.99362 / 0.42)
color-mix(in oklab, … 42%, transparent) → oklab(0.62 0.12 -0.207846 / 0.42)
On screen, all three look the same. But only alpha() gives you back what you put in: an OKLCh color stays OKLCh. That matters if you inspect tokens in DevTools or feed computed values into other color math. With a hex origin, the same call in Chromium returns rgba(255, 82, 0, 0.4), which is still the sRGB the hex value came from.
Browser support and gotchas
- Support: Chrome and Edge 152, Firefox 155, Safari 27 (desktop and iOS), according to MDN's browser-compat-data. That makes it Baseline "newly available" from September 14, 2026, so a lot of browsers in use today still won't understand it.
-
Fallback: the classic trick doesn't work here. Normally you write a fallback declaration first, and if the browser can't parse the second one, it ignores it. But
alpha(from var(--brand) / 16%)containsvar(), so the browser can't check it until computed-value time. When it fails then, MDN explains that "the declaration is not ignored": the property falls back to its initial or inherited value. I tested the same pattern in Chromium with a made-up function:background: red; background: notafunction(from var(--brand) / 16%)ends up transparent, not red. So use@supports.color-mix()has been Baseline since May 2023, which makes it a safe plan B. This is what the demo does:
.glass { background: alpha(from var(--brand) / 16%); }
@supports not (color: alpha(from red / 50%)) {
.glass { background: color-mix(in oklab, var(--brand) 16%, transparent); }
}
The demo checks the same thing in JavaScript with CSS.supports('color', 'alpha(from red / 50%)') and shows a notice when it falls back.
-
Tokens resolve where they're declared. If you write
--brand-soft: alpha(from var(--brand) / 20%)on:rootand then override--brandinside.teal,var(--brand-soft)in that section still uses the root color. I checked this in Chromium: the token stayed orange, while the samealpha()written directly on the element turned teal. Either declare the derived tokens on the element where--brandchanges, or usealpha()inline. -
It's
from, not a comma.alpha(red, 50%)is invalid. Thefromkeyword and the slash are both required.
What to keep an eye on
- "Widely available" status. Baseline "newly available" means every engine has shipped it, not that every user has updated. Keep the fallback for a while.
-
Design tools catching up. Token pipelines and design apps that export
rgba()copies of every shade could export a single color plusalpha()calls. -
CSS Color 5 as a whole.
alpha()is new in Level 5, alongside relative color syntax andcolor-mix(). It's the smallest function of the set, and probably the one you'll write most often.
Sources
- alpha() CSS function, MDN
- CSS Color Module Level 5: Relative Alpha Colors
- Using relative colors, MDN
- CSS error handling: invalid custom properties, MDN
Originally published at richi.codes.
Top comments (3)
Solid case for alpha(), and the "one variable, every alpha" framing is the right one. One thing worth adding: alpha() keeps the components, so the same percentage does very different amounts of work depending on what sits underneath. I measured this in CIELAB chroma (C*, how much color is actually visible):
rgba(59,111,214,0.08)over#ffffffgives#eff3fc, C* 4.800.08over#111827gives#141f35, C* 16.21So a 5 to 8% wash that reads correctly on a light card is roughly three to four times stronger on a dark hero. That is compositing working as designed, not a bug in alpha(), but it means "one alpha token" only measures the same thing when the surface underneath is fixed. Where a tint has to look identical over both, sample the composited hex and ship that value, and keep alpha() for borders and glows where the background is predictable. On the low end, worth remembering that 75 of the 100 background colors I collected sit under C* 1, which is visually gray. A 6% wash is easy to convince yourself you can see when the hex says blue.
Muted SVG strokes can use
alpha(from currentColor / 65%), keeping them theme-aware. Still worth checking contrast in both themes.currentColor plus alpha is the right move for strokes, but the same 65% does not land the same in both themes: 8% of a brand blue composites to C* 4.80 on white and 16.21 on a near-black surface. So a theme-aware stroke still wants a per-theme value if the line weight has to read the same.