I fixed a CSSOM bug in Chromium: CSSImportRule.media

My first Chromium commit, shipping in Chrome 157: CSSImportRule.media and styleSheet.media are now the same object, as the CSSOM spec requires. Here is how.

I fixed a CSSOM bug in Chromium: CSSImportRule.media

On 9 October 2026 my first change to Chromium was merged. It is small, six lines in Blink's CSS engine and six more in the DevTools agent, but it closes a gap between Chrome and the CSSOM specification that had been open for years. From Chrome 157, CSSImportRule.media and CSSImportRule.styleSheet.media are the same object, as the spec requires. This post is the story of that change: the bug, the fix, and what the Chromium review process looks like from the outside.

The bug

Take a style sheet that imports another one with a media query:

@import url("print.css") print;

In JavaScript, that @import is a CSSImportRule. It has two ways to reach the media queries: rule.media, and rule.styleSheet.media on the imported sheet. Before the fix, Chrome gave two different answers:

const rule = document.styleSheets[0].cssRules[0];

rule.media.mediaText;             // "print"
rule.styleSheet.media.mediaText;  // "" (empty)
rule.media === rule.styleSheet.media; // false

The first accessor was right. The second one returned a brand new, empty MediaList. Anything that walked style sheets instead of rules, such as a CSS tooling library, a polyfill, or a test harness, saw an imported sheet with no media at all. Firefox returned the same object from both accessors. Chrome and Edge did not.

What the spec says

The CSSOM specification is explicit about this. For CSSImportRule, the media attribute "must return the value of the media attribute of the associated CSS style sheet". In other words, the import rule does not own a media list of its own. It exposes the sheet's. The two properties have to be the same object, and an edit through one has to be visible through the other.

There was already a web platform test for exactly this, css/cssom/CSSImportRule-media-identity.html. It asserts that rule.media and rule.styleSheet.media are equal and that the media text is preserved. Chrome had it marked as an expected failure.

Why Blink got it wrong

In Blink, the media queries of an imported sheet are stored on the StyleRuleImport, the internal object behind the @import rule. CSSImportRule::media() read them from there, so it was correct. CSSStyleSheet::media() knew nothing about the import rule. When asked for its media, the imported sheet lazily created its own MediaQuerySet, which was empty, and wrapped it in a new MediaList. Two objects, one of them always blank.

The fix

The imported sheet already knows which rule owns it. The fix makes CSSStyleSheet::media() hand the question back to that rule when the owner is a CSSImportRule:

MediaList* CSSStyleSheet::media() {
  // https://drafts.csswg.org/cssom/#dom-cssimportrule-media
  // CSSImportRule.media must return the media attribute of the associated
  // style sheet, so an imported sheet shares the import rule's MediaList.
  if (auto* import_rule = DynamicTo<CSSImportRule>(owner_rule_.Get())) {
    return import_rule->media();
  }
  if (!media_queries_) {
    media_queries_ = MediaQuerySet::Create();
  }
  // ... existing code
}

Reads and writes through that MediaList already went to the import rule, because the rule was its MediaQuerySetOwner. So sharing the object was enough. Sheets owned by a <style> or <link> element, and constructed sheets, keep their own media list and are not affected.

There was one side effect to handle. DevTools builds the list of media queries for the Styles panel by walking both rules and sheets. With the sheet now reporting the import's media, the same query would have shown up twice. The DevTools CSS agent already reported an @import's media from the import rule, with the correct source position, so I made it skip sheets that have an owner rule:

void InspectorCSSAgent::CollectMediaQueriesFromStyleSheet(...) {
  // A sheet loaded via @import shares its MediaList with the CSSImportRule.
  // That media is reported from the import rule itself.
  if (style_sheet->ownerRule()) {
    return;
  }
  // ... existing code
}

DevTools output is unchanged, and the expected-failure file for the web platform test is gone because the test now passes.

How the review went

I uploaded the change to Chromium's Gerrit on 22 September. A few things are worth knowing if you have never done this:

  • Your first change adds your name to the AUTHORS file, after signing the contributor license agreement.
  • External contributors cannot start the try bots. A committer has to trigger the dry run for you, which Rune Lillesveen from the Blink style team did within the hour.
  • My first version also overrode two media-query methods on the sheet. Review feedback was that the override was unnecessary once the MediaList was shared, so the final patch set dropped them. The merged change is smaller than the first one.
  • The commit message matters as much as the code. It states the spec, the bug it fixes, what is affected and what is not, and the DevTools consequence. Reviewers read that first.

Rune Lillesveen and Morten Stenshorne reviewed it, the commit queue rebased and submitted it on 9 October, 17 days after upload, as commit position 1715398. The first Chrome build with the fix is 157.0.8095.0.

Try it

In Chrome Canary 157 or later, open DevTools on any page and run:

const style = document.createElement("style");
style.textContent = '@import url("data:text/css,") screen;';
document.head.append(style);

await new Promise(r => setTimeout(r, 100));
const rule = style.sheet.cssRules[0];
console.log(rule.media === rule.styleSheet.media); // true
console.log(rule.styleSheet.media.mediaText);      // "screen"

On Chrome 156 and earlier the first line prints false and the second prints an empty string.

Why bother with a six-line fix

Because browser engines are made of thousands of six-line decisions, and the spec only matters if the implementations agree. Finding a failing web platform test, reading the spec sentence it checks, and tracing why the engine disagrees is a very good way to learn how a browser actually works. It is also open to anyone. If you write CSS or JavaScript for a living and have never looked at the engine underneath, pick a failing test on wpt.fyi and start there.

Links: the change on Gerrit, the CSSOM spec, and the web platform test.

Frequently asked questions

What does CSSImportRule.media return?

The MediaList of the style sheet imported by that @import rule. Per the CSSOM spec, `rule.media` and `rule.styleSheet.media` are the same object, so reading or editing either one gives the same result.

Which Chrome version includes the fix?

The change landed in Chromium on 9 October 2026 and first appears in Chrome 157.0.8095.0. Edge picks it up with the same Chromium version. Firefox already behaved correctly.

Does this change break existing websites?

No. Code that read `importRule.media` keeps working. Code that read `importRule.styleSheet.media` now gets the real media queries instead of an empty list, which is what the spec and Firefox already did.

Can an external developer contribute to Chromium?

Yes. You need a Google account, the Chromium contributor license agreement, a checkout of the source, and a change uploaded to Gerrit. A Chromium committer runs the try bots and reviews it. My change took 17 days from upload to merge.