stylo: servo has no user-preference media features, and its system colors are fixed
Generated from repros/stylo-servo-user-preference-media-features/README.md when the site was built at 7aa23e2.
What niso does
Section titled “What niso does”No workaround yet. DECISIONS.md:34 makes the platform’s user settings live CSS inputs: Increase Contrast is
prefers-contrast: more, reduced motion and transparency are their media queries, and the iOS tint and Material
You palette are the system colors AccentColor and AccentColorText. Servo-engine stylo, which blitz uses, has
neither:
style/servo/media_features.rs:163lists 15 features and none ofprefers-reduced-motion,prefers-reduced-transparencyorprefers-contrast, so they parse as<general-enclosed>and never match.@media (prefers-reduced-motion: no-preference)is false on every device, so the radio example’s drift animation, written to run only then, never runs.Device::system_color(style/device/servo.rs:377) returns hard-coded light and dark values (AccentColoris always#0066cc, or#0a84ffwhen dark), andMediaDatahas no field for the host to supply its own.
niso can only send the platform palette as custom properties (Dom::set_platform_style), which pages that use
AccentColor don’t see.
Expected vs actual
Section titled “Expected vs actual”$ cargo runexpected: each is a known media feature(prefers-reduced-motion: no-preference) unknown <general-enclosed>, never matches(prefers-reduced-motion: reduce) unknown <general-enclosed>, never matches(prefers-reduced-transparency: reduce) unknown <general-enclosed>, never matches(prefers-contrast: more) unknown <general-enclosed>, never matches(prefers-contrast: no-preference) unknown <general-enclosed>, never matchescargo run --features host-preferences builds the embedder side of the patch below: a MediaData carrying the
user’s preferences and an AccentColor of its own. Without the patch it does not compile, because MediaData
has no prefers_* or system_colors field and there is no other way to set them.
CARGO_TARGET_DIR=~/.capy/work/repro-target cargo runExit status 1 while the bug reproduces.
The patch
Section titled “The patch”servo-user-preferences.patch is against stylo 2dca345 (5 files, +138 -34) and also applies cleanly to main
7f0189de128e6dbd2b504acd3aebee3befa78358. Apply it from the stylo root with git apply or patch -p1.
prefers-contrast,prefers-reduced-motionandprefers-reduced-transparencyfor the servo engine, read from newMediaDatafields. The value enums move fromgecko/media_features.rstoqueries/values.rs, shared by both engines besidePrefersColorScheme, and gainDefault(no-preference). This is the shape of servo/stylo#288, rebased ontoMediaData.MediaData::system_colors: Option<Arc<SystemColors>>holds light and dark lists indexed bySystemColor as usize.Device::system_colorreturns the host’s entry when there is one and the current default otherwise. TheArckeepsMediaDatacheap to clone and keeps itClone + PartialEq + Debug.
Adding public fields to MediaData breaks embedders that build it with a struct literal, as blitz does
(blitz-dom/src/stylo_device.rs:76), so blitz has to set the new fields too.
With the patch applied to a copy of the stylo source, and a copy of this repro pointed at it through
[patch."https://github.com/servo/stylo"] (stylo and stylo_traits by path):
$ cargo runexpected: each is a known media feature(prefers-reduced-motion: no-preference) known media feature...$ cargo run --features host-preferences...host: reduced motion, no transparency preference, more contrast(prefers-reduced-motion: reduce) matches: true (expected true)(prefers-reduced-motion: no-preference) matches: false (expected false)(prefers-reduced-motion) matches: true (expected true)(prefers-reduced-transparency) matches: false (expected false)(prefers-contrast: more) matches: true (expected true)(prefers-contrast) matches: true (expected true)light AccentColor computes to rgb(255, 45, 85) (expected rgb(255, 45, 85))light Canvas computes to rgb(255, 255, 255) (expected rgb(255, 255, 255))dark AccentColor computes to rgb(255, 55, 95) (expected rgb(255, 55, 95))Both exit 0. The gecko half of the enum move is not built here, since that needs a Firefox tree; it only replaces three local enums with imports of identical ones.
Upstream main
Section titled “Upstream main”Reproduces on servo/stylo main at 7f0189de128e6dbd2b504acd3aebee3befa78358: MEDIA_FEATURES is still the
same 15, and Device::system_color still hard-codes every color with no embedder input.
Existing issue or PR
Section titled “Existing issue or PR”- https://github.com/servo/stylo/pull/288 (open), “Add remaining preference media queries” by lukewarlow, was
approved by mrobinson in January 2026 and never merged. It predates
MediaData(it adds the values asDevicefields and constructor arguments) and no longer applies; jdm asked for a rebase in June 2026. The Servo side is https://github.com/servo/servo/pull/42011 (open), tracked in https://github.com/servo/servo/issues/39068. - https://github.com/servo/stylo/issues/477 (open) asks for these and more servo-mode media features. Loirooriol
suggested putting them behind a pref with
FeatureFlags::CHROME_AND_UA_ONLYwhen it’s off. - For system colors, https://github.com/servo/stylo/pull/302 (merged) added today’s fixed values, and https://github.com/servo/servo/issues/15973 (open) says Servo should eventually query the OS for them. Nothing proposes an embedder hook.
Issue text
Section titled “Issue text”To post on servo/stylo#288, or as a new PR that references it:
Title: servo: user-preference media features from MediaData, and embedder-provided system colors
Body:
Servo-engine stylo has no prefers-reduced-motion, prefers-reduced-transparency or prefers-contrast, so
@media (prefers-reduced-motion: no-preference) never matches, and Device::system_color hard-codes every
system color, so an embedder can’t give AccentColor the platform’s tint. #288 covered the media features but
predates MediaData. This rebases it onto MediaData and adds the colors:
MediaDatagainsprefers_reduced_motion,prefers_reduced_transparencyandprefers_contrast(enums moved toqueries/values.rsas in #288, now withDefault=no-preference), evaluated by three new entries inMEDIA_FEATURES.MediaDatagainssystem_colors: Option<Arc<SystemColors>>, light and dark lists indexed bySystemColor as usize.Device::system_coloruses an entry when the embedder supplied one and the current default otherwise.
// Cargo.toml: style = { package = "stylo", git = "https://github.com/servo/stylo", rev = "..." },// style_traits = { package = "stylo_traits", ... }, cssparser = "0.38", url = "2.5"use cssparser::Parser;use style::context::QuirksMode;use style::media_queries::MediaList;use style::parser::ParserContext;use style::queries::QueryCondition;use style::stylesheets::{CssRuleType, Origin, UrlExtraData};use style_traits::ParsingMode;
fn main() { let url = UrlExtraData::from(url::Url::parse("about:blank").unwrap()); let mut context = ParserContext::new(Origin::Author, &url, Some(CssRuleType::Media), ParsingMode::DEFAULT, QuirksMode::NoQuirks, Default::default(), None, None, Default::default()); let list = MediaList::parse(&mut context, &mut Parser::new("(prefers-reduced-motion: no-preference)")); // Fails: the condition is GeneralEnclosed, so it never matches. assert!(matches!(list.media_queries[0].condition, Some(QueryCondition::Feature(_))));}Tested at 2dca34592ced37c6aa01733e94206a767194cf9d; the patch applies unchanged to main
7f0189de128e6dbd2b504acd3aebee3befa78358. Adding MediaData fields is a breaking change for embedders that
build it with a struct literal (Servo, Blitz).