commit 1975a2d7f60a2fd5cad5662103e305cc15327f50
parent 604a2486ead5757007ccb33c160e16a14fc21e19
Author: Jonathan Kew <jkew@mozilla.com>
Date: Tue, 28 Oct 2025 14:56:47 +0000
Bug 1682439 - Implement CSS contrast-color() function. r=firefox-style-system-reviewers,emilio,tlouw
This implements the contrast-color() function from CSS Color 5
(https://drafts.csswg.org/css-color-5/#contrast-color).
The extended version proposed in CSS Color 6 is not sufficiently well spec'd
to consider implementing at this time.
Differential Revision: https://phabricator.services.mozilla.com/D269883
Diffstat:
7 files changed, 77 insertions(+), 54 deletions(-)
diff --git a/layout/inspector/tests/test_bug877690.html b/layout/inspector/tests/test_bug877690.html
@@ -46,6 +46,7 @@ function do_test() {
"hwb",
"color-mix",
"color",
+ "contrast-color",
"lab",
"lch",
"light-dark",
diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml
@@ -9969,6 +9969,13 @@
mirror: always
rust: true
+# Is support for the contrast-color() function enabled?
+- name: layout.css.contrast-color.enabled
+ type: RelaxedAtomicBool
+ value: true
+ mirror: always
+ rust: true
+
# Whether alt text in content is enabled.
- name: layout.css.content.alt-text.enabled
type: RelaxedAtomicBool
diff --git a/servo/components/style/values/computed/color.rs b/servo/components/style/values/computed/color.rs
@@ -34,6 +34,11 @@ impl ToCss for Color {
Self::ColorFunction(ref color_function) => color_function.to_css(dest),
Self::CurrentColor => dest.write_str("currentcolor"),
Self::ColorMix(ref m) => m.to_css(dest),
+ Self::ContrastColor(ref c) => {
+ dest.write_str("contrast-color(")?;
+ c.to_css(dest)?;
+ dest.write_char(')')
+ },
}
}
}
@@ -81,6 +86,42 @@ impl Color {
mix.flags,
)
},
+ Self::ContrastColor(ref c) => {
+ let bg_color = c.resolve_to_absolute(current_color);
+ if Self::contrast_ratio(&bg_color, &AbsoluteColor::BLACK)
+ > Self::contrast_ratio(&bg_color, &AbsoluteColor::WHITE)
+ {
+ AbsoluteColor::BLACK
+ } else {
+ AbsoluteColor::WHITE
+ }
+ },
+ }
+ }
+
+ fn contrast_ratio(a: &AbsoluteColor, b: &AbsoluteColor) -> f32 {
+ // TODO: This just implements the WCAG 2.1 algorithm,
+ // https://www.w3.org/TR/WCAG21/#dfn-contrast-ratio
+ // Consider using a more sophisticated contrast algorithm, e.g. see
+ // https://apcacontrast.com
+ let compute = |c| -> f32 {
+ if c <= 0.04045 {
+ c / 12.92
+ } else {
+ f32::powf((c + 0.055) / 1.055, 2.4)
+ }
+ };
+ let luminance = |r, g, b| -> f32 { 0.2126 * r + 0.7152 * g + 0.0722 * b };
+ let a = a.into_srgb_legacy();
+ let b = b.into_srgb_legacy();
+ let a = a.raw_components();
+ let b = b.raw_components();
+ let la = luminance(compute(a[0]), compute(a[1]), compute(a[2])) + 0.05;
+ let lb = luminance(compute(b[0]), compute(b[1]), compute(b[2])) + 0.05;
+ if la > lb {
+ la / lb
+ } else {
+ lb / la
}
}
}
diff --git a/servo/components/style/values/generics/color.rs b/servo/components/style/values/generics/color.rs
@@ -24,6 +24,8 @@ pub enum GenericColor<Percentage> {
CurrentColor,
/// The color-mix() function.
ColorMix(Box<GenericColorMix<Self, Percentage>>),
+ /// The contrast-color() function.
+ ContrastColor(Box<Self>),
}
/// Flags used to modify the calculation of a color mix result.
diff --git a/servo/components/style/values/specified/color.rs b/servo/components/style/values/specified/color.rs
@@ -125,6 +125,8 @@ pub enum Color {
ColorMix(Box<ColorMix>),
/// A light-dark() color.
LightDark(Box<GenericLightDark<Self>>),
+ /// The contrast-color function.
+ ContrastColor(Box<Color>),
/// Quirksmode-only rule for inheriting color from the body
#[cfg(feature = "gecko")]
InheritFromBodyQuirk,
@@ -455,6 +457,17 @@ impl Color {
return Ok(Color::LightDark(Box::new(ld)));
}
+ if static_prefs::pref!("layout.css.contrast-color.enabled") {
+ if let Ok(c) = input.try_parse(|i| {
+ i.expect_function_matching("contrast-color")?;
+ i.parse_nested_block(|i| {
+ Self::parse_internal(context, i, preserve_authored)
+ })
+ }) {
+ return Ok(Color::ContrastColor(Box::new(c)));
+ }
+ }
+
match e.kind {
ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(t)) => {
Err(e.location.new_custom_error(StyleParseErrorKind::ValueError(
@@ -529,6 +542,11 @@ impl ToCss for Color {
Color::ColorFunction(ref color_function) => color_function.to_css(dest),
Color::ColorMix(ref mix) => mix.to_css(dest),
Color::LightDark(ref ld) => ld.to_css(dest),
+ Color::ContrastColor(ref c) => {
+ dest.write_str("contrast-color(")?;
+ c.to_css(dest)?;
+ dest.write_char(')')
+ },
#[cfg(feature = "gecko")]
Color::System(system) => system.to_css(dest),
#[cfg(feature = "gecko")]
@@ -563,6 +581,7 @@ impl Color {
mix.left.honored_in_forced_colors_mode(allow_transparent)
&& mix.right.honored_in_forced_colors_mode(allow_transparent)
},
+ Self::ContrastColor(ref c) => c.honored_in_forced_colors_mode(allow_transparent),
}
}
@@ -768,6 +787,9 @@ impl Color {
flags: mix.flags,
})
},
+ Color::ContrastColor(ref c) => {
+ ComputedColor::ContrastColor(Box::new(c.to_computed_color(context)?))
+ },
#[cfg(feature = "gecko")]
Color::System(system) => system.compute(context?),
#[cfg(feature = "gecko")]
@@ -803,6 +825,9 @@ impl ToComputedValue for Color {
ComputedColor::ColorMix(ref mix) => {
Color::ColorMix(Box::new(ToComputedValue::from_computed_value(&**mix)))
},
+ ComputedColor::ContrastColor(ref c) => {
+ Self::ContrastColor(Box::new(ToComputedValue::from_computed_value(&**c)))
+ },
}
}
}
@@ -830,6 +855,7 @@ impl SpecifiedValueInfo for Color {
"oklab",
"oklch",
"color-mix",
+ "contrast-color",
"light-dark",
]);
}
diff --git a/testing/web-platform/meta/css/css-color/parsing/color-computed-contrast-color-function.html.ini b/testing/web-platform/meta/css/css-color/parsing/color-computed-contrast-color-function.html.ini
@@ -1,30 +1,3 @@
[color-computed-contrast-color-function.html]
- [Property background-color value 'contrast-color(white)']
- expected: FAIL
-
- [Property background-color value 'contrast-color(black)']
- expected: FAIL
-
- [Property background-color value 'contrast-color(pink)']
- expected: FAIL
-
- [Property background-color value 'contrast-color(color(srgb 1 0 1 / 0.5))']
- expected: FAIL
-
- [Property background-color value 'contrast-color(lab(0.2 0.5 0.2))']
- expected: FAIL
-
- [Property background-color value 'contrast-color(color(srgb 10 10 10))']
- expected: FAIL
-
- [Property background-color value 'contrast-color(color(srgb -10 -10 -10))']
- expected: FAIL
-
- [Property background-color value 'contrast-color(contrast-color(pink))']
- expected: FAIL
-
- [Property background-color value 'contrast-color(currentcolor)']
- expected: FAIL
-
[Property background-color value 'contrast-color(color(srgb calc(1 + (sign(20cqw - 10px) * 1)) calc(1 + (sign(20cqw - 10px) * 1)) calc(1 + (sign(20cqw - 10px) * 1))))']
expected: FAIL
diff --git a/testing/web-platform/meta/css/css-color/parsing/color-valid-contrast-color-function.html.ini b/testing/web-platform/meta/css/css-color/parsing/color-valid-contrast-color-function.html.ini
@@ -1,30 +1,3 @@
[color-valid-contrast-color-function.html]
- [e.style['background-color'\] = "contrast-color(white)" should set the property value]
- expected: FAIL
-
- [e.style['background-color'\] = "contrast-color(black)" should set the property value]
- expected: FAIL
-
- [e.style['background-color'\] = "contrast-color(pink)" should set the property value]
- expected: FAIL
-
- [e.style['background-color'\] = "contrast-color(color(srgb 1 0 1 / 0.5))" should set the property value]
- expected: FAIL
-
- [e.style['background-color'\] = "contrast-color(lab(0.2 0.5 0.2))" should set the property value]
- expected: FAIL
-
- [e.style['background-color'\] = "contrast-color(color(srgb 10 10 10))" should set the property value]
- expected: FAIL
-
- [e.style['background-color'\] = "contrast-color(color(srgb -10 -10 -10))" should set the property value]
- expected: FAIL
-
- [e.style['background-color'\] = "contrast-color(contrast-color(pink))" should set the property value]
- expected: FAIL
-
- [e.style['background-color'\] = "contrast-color(currentcolor)" should set the property value]
- expected: FAIL
-
[e.style['background-color'\] = "contrast-color(color(srgb calc(0.5) calc(1 + (sign(20cqw - 10px) * 0.5)) 1 / .5))" should set the property value]
expected: FAIL