Treat HWB as an sRGB editing model
HWB means hue, whiteness, and blackness. CSS Color 4 defines it as another way to specify an sRGB color: begin with the fully saturated sRGB hue, mix in a proportion of white, and reserve a proportion for black. The result remains inside sRGB, so HWB does not unlock Display P3 or other wide-gamut colors. Its value is operational. A picker can expose a stable hue while a person adjusts how much of the result comes from the white and black ends of the range.
- Hue uses the same circle as HSL, including the same uneven visual spacing between numeric angles.
- Whiteness raises the minimum sRGB channel and moves the result toward white while the color remains opaque.
- Blackness lowers the maximum available channel and moves the result toward black while the color remains opaque.
- HWB resolves to sRGB, so an equivalent HEX or rgb() value can represent every opaque HWB result.
- HWB coordinates are editing controls, not accessibility evidence; contrast must be calculated from the resolved rendered colors.
Write the space-separated syntax and keep alpha separate
The function is written hwb(H W B / A). Hue accepts an angle or a unitless number interpreted as degrees; angles outside one turn wrap to the same position. Whiteness and blackness are clearest as percentages. Alpha follows a slash and defaults to 100% when omitted. Unlike hsl(), HWB has no legacy comma-separated form: commas inside hwb() make the value invalid. Modern CSS also permits the none keyword for a missing component, but none is not a synonym for zero and is mainly relevant when colors are interpolated.
:root {
--action: hwb(210 10% 20%);
--action-translucent: hwb(210 10% 20% / 60%);
--action-wrapped-hue: hwb(570deg 10% 20%); /* Same hue as 210deg */
}
/* Invalid: HWB does not have a comma syntax. */
.invalid-example {
color: hwb(210, 10%, 20%);
}| HWB input | Resolved HEX | Resolved RGB | What it demonstrates |
|---|---|---|---|
| hwb(0 0% 0%) | #FF0000 | 255 0 0 | Pure red hue |
| hwb(120 0% 0%) | #00FF00 | 0 255 0 | Pure green hue |
| hwb(240 0% 0%) | #0000FF | 0 0 255 | Pure blue hue |
| hwb(210 10% 20%) | #1A73CC | 26 115 204 | White and black mixed into the base hue |
| hwb(570 10% 20%) | #1A73CC | 26 115 204 | Hue wraps by 360 degrees |
Raise whiteness and watch the foreground threshold
Hold hue at 210 degrees and blackness at 20%, then increase whiteness. The largest resolved channel stays at 80% while the smaller channels rise. The table follows the CSS Color 4 HWB-to-sRGB algorithm, rounds each channel to the nearest 8-bit value, and then calculates contrast from that rounded HEX with the WCAG relative-luminance formula. AA for normal text requires at least 4.5:1.
| HWB | 8-bit sRGB | Black text | White text | AA normal choice |
|---|---|---|---|---|
| hwb(210 0% 20%) | #0066CC | 3.77:1 | 5.57:1 | White |
| hwb(210 10% 20%) | #1A73CC | 4.37:1 | 4.81:1 | White |
| hwb(210 20% 20%) | #3380CC | 5.10:1 | 4.12:1 | Black |
| hwb(210 30% 20%) | #4D8CCC | 5.94:1 | 3.54:1 | Black |
| hwb(210 40% 20%) | #6699CC | 6.99:1 | 3.00:1 | Black |
| hwb(210 60% 20%) | #99B3CC | 9.67:1 | 2.17:1 | Black |
- The foreground changes between 10% and 20% whiteness for this particular hue and blackness.
- At 10% whiteness, black reaches only 4.37:1 while white reaches 4.81:1; rounding the input percentage does not make both safe.
- The 40% step gives black 6.99:1, which passes AA but is just below the 7:1 AAA threshold for normal text.
- Test every semantic state independently; the same foreground cannot be assumed safe across a generated HWB ramp.
- Keep unrounded contrast values for pass/fail decisions and round only the displayed documentation.
Raise blackness without confusing it with opacity
Now hold hue at 210 degrees and whiteness at 10%. Increasing blackness reduces the available contribution from the pure hue while the minimum channel remains near 10%. Every row is still fully opaque. That is fundamentally different from lowering alpha: a translucent color reveals an unknown backdrop, but a higher-blackness HWB value already resolves to a complete sRGB triplet. The foreground switches in the opposite direction as the scale gets darker.
| HWB | 8-bit sRGB | Black text | White text | AA normal choice |
|---|---|---|---|---|
| hwb(210 10% 0%) | #1A8CFF | 6.24:1 | 3.37:1 | Black |
| hwb(210 10% 10%) | #1A80E6 | 5.27:1 | 3.98:1 | Black |
| hwb(210 10% 20%) | #1A73CC | 4.37:1 | 4.81:1 | White |
| hwb(210 10% 30%) | #1A66B3 | 3.60:1 | 5.84:1 | White |
| hwb(210 10% 40%) | #1A5999 | 2.93:1 | 7.16:1 | White |
| hwb(210 10% 50%) | #1A4D80 | 2.42:1 | 8.69:1 | White |
| hwb(210 10% 60%) | #1A4066 | 1.97:1 | 10.66:1 | White |
Normalize whiteness plus blackness at 100%
The most important HWB edge case appears when W + B is at least 100%. CSS does not reject the color. Instead, it normalizes the two amounts by their sum and returns an achromatic result whose three sRGB channels equal W / (W + B). The original hue cannot affect that gray and is therefore powerless. This means values with different totals but the same W:B ratio can resolve to the same color once they cross the boundary.
| HWB input | W + B | Normalized W:B | Resolved HEX | Hue result |
|---|---|---|---|---|
| hwb(45 40% 80%) | 120% | 33.33% : 66.67% | #555555 | Powerless |
| hwb(200 50% 50%) | 100% | 50% : 50% | #808080 | Powerless |
| hwb(10 100% 100%) | 200% | 50% : 50% | #808080 | Powerless |
| hwb(0 70% 70%) | 140% | 50% : 50% | #808080 | Powerless |
| hwb(320 80% 20%) | 100% | 80% : 20% | #CCCCCC | Powerless |
- The first row is the normalization example defined in CSS Color 4: 40 / (40 + 80) produces one-third gray.
- Equal whiteness and blackness always normalize to 50% gray once their sum reaches the boundary.
- Do not encode a semantic hue in an achromatic token; changing the angle cannot change the rendered result.
- If a generated scale is intended to retain hue, validate W + B < 100% before publishing it.
- Avoid repeated round trips through rounded HWB percentages because the W:B ratio can drift near the boundary.
Convert between RGB, HWB, and HSL without losing the source
For an sRGB color, HWB whiteness is the smallest normalized RGB channel and blackness is one minus the largest channel. Hue is derived from the channel difference using the same hue geometry as HSL. The conversion is deterministic, but display-friendly rounding is not reversible. Preserve one canonical representation at sufficient precision, then derive the alternatives for documentation and copy controls instead of repeatedly overwriting the source.
| HEX | RGB | Derived HWB | Derived HSL | Conversion note |
|---|---|---|---|---|
| #663399 | 102 51 153 | hwb(270 20% 40%) | hsl(270 50% 40%) | Exact integer percentages |
| #FFA500 | 255 165 0 | hwb(38.82 0% 0%) | hsl(38.82 100% 50%) | Pure HWB hue, fractional angle |
| #00FFFF | 0 255 255 | hwb(180 0% 0%) | hsl(180 100% 50%) | Pure cyan hue |
| #808080 | 128 128 128 | hwb(none 50.20% 49.80%) | hsl(none 0% 50.20%) | Hue is powerless |
| #4F46E5 | 79 70 229 | hwb(243.40 27.45% 10.20%) | hsl(243.40 75.35% 58.63%) | Keep decimals for a reversible audit |
Ship HWB tokens with resolved evidence
A practical HWB token set keeps the editable coordinates, a resolved sRGB fallback, and tested foreground results together. Direct property declarations can put HEX first and HWB second, allowing software that does not understand the later value to retain the earlier declaration. This fallback does not solve alpha contrast and it does not make an untested scale accessible; every final rendered state still requires its own evidence.
:root {
--action: hwb(210 10% 20%);
--action-muted: hwb(210 30% 20%);
--action-dark: hwb(210 10% 40%);
--on-action: #FFFFFF;
--on-action-muted: #000000;
}
.action {
color: #FFFFFF;
background: #1A73CC;
background: var(--action); /* 4.81:1 with white */
}
.action-muted {
color: var(--on-action-muted);
background: #4D8CCC;
background: var(--action-muted); /* 5.94:1 with black */
}- Keep W + B below 100% whenever the selected hue must remain visible.
- Store the canonical HWB coordinates with enough precision and export rounded HEX as a derived sRGB value.
- Test normal, hover, active, selected, focus, disabled, light-theme, and dark-theme foreground pairs separately.
- Resolve alpha against the actual backdrop before calculating luminance or contrast.
- Do not call HWB wide-gamut or perceptually uniform; it inherits the gamut and hue limitations of its sRGB foundation.
- Use HSL when saturation and model lightness are the intended controls, and OKLCH when perceptual spacing is the requirement.
- Link the conversion specification and retain unrounded audit values so a future generator can reproduce the result.
Primary sources
The factual claims in this article are checked against these published specifications.