Solved by StyleObserver: Element.matchContainer()

Martin Winkler published a package that polyfills Element.matchContainer to get notified in script when a Container Query matches/unmatches. Under the hood, it uses a StyleObserver.

~

Window.matchMedia()

With Window.matchMedia() you can observe whether a Media Query matches the given document.

const mql = window.matchMedia("(max-width: 600px)");
console.log(mql.matches); // true when the document is ≤ 600px

Furthermore you can use it to observe changes, by listening to the change event on the returned MediaQueryList object.

const mql = window.matchMedia("(max-width: 600px)");
mql.addEventListener("change", (e) => {
  console.log(e.matches);
});

~

Element.matchContainer()

One of the open requests at the CSS Working Group is to have something similar to Window.matchMedia() but then for Container Queries. One of the suggested solutions is to have something like Element.matchContainer().

Today, Martin Winkler published a package that polyfills exactly that: match-container:

Element.matchContainer() is to @container what Window.matchMedia() is to the @media query CSS feature.

It allows you to do things like this:

import "https://esm.sh/match-container";

const cql = $someElement.matchContainer("(width < 400px)");
cql.addEventListener("change", (e) => {
  console.log(e.matches);
});

Or this:

import "https://esm.sh/match-container";

const cql = $someElement.matchContainer("style(--my-property: some-value)");
cql.addEventListener("change", (e) => {
  console.log(e.matches);
});

Sweet!

~

StyleObserver under the hood

Peeking under the hood, I see Martin uses my technique from @bramus/style-observer: set up a transition on some custom properties and listen for their transitionrun event. These custom properties automatically get registered and applied on the registered Container Query.

When Element.matchContainer() is called the polyfill inserts a corresponding @container query into the CSSOM. The observed element is tethered to an attribute selector inside this query block by a corresponding data-* attribute, ensuring the target element is the only element matched by this selector. When the @container query matches a prepared CSS custom property is set on the observed element. A style observer (heavily inspired by Bramus’ StyleObserver) finally triggers the callback in the script code.

For example, if you observe "(width < 400px)", this piece of CSS automatically gets injected into the page.

@property --container-query-observer-bae45330cd3d4e0e96b60d26b57009b5-1740410397140-0 {
  syntax: "<custom-ident>";
  inherits: false;
  initial-value: --false;
}

@container (width < 400px) {
  [data-container-query-observer-bae45330cd3d4e0e96b60d26b57009b5-1740410397140-0] {
    --container-query-observer-bae45330cd3d4e0e96b60d26b57009b5-1740410397140-0: --true;
  }
}

The attribute you see there is also auto-generated and gets attached to the observed element.

~

Demos

Here are two demos that use match-container. The first one uses a size query and the second one a style query. Whenever the container query matches/unmatches, the log gets prepended with a new entry.

See the Pen
Element.matchContainer() demo
by Bramus (@bramus)
on CodePen.

See the Pen
Element.matchContainer() demo – Style Query
by Bramus (@bramus)
on CodePen.

‼️ Don’t forget that container queries query a parent Element, so you need to attach .matchContainer on a child of the container (!).

Also don’t forget to apply container-type: inline-size when doing .matchContainer(<size-query>).

~

In Closing

It’s very nice to see that Martin got to leverage the technique from @bramus/style-observer to solve this longstanding request. It confirms to me once again that we need a native StyleObserver built into the Web Platform.

You can get match-container from NPM and check out its source code on GitHub.

~

# Spread the word

Feel free to reshare one of the following posts on social media to help spread the word:

~

Published by Bramus!

Bramus is a frontend web developer from Belgium, working as a Chrome Developer Relations Engineer at Google. From the moment he discovered view-source at the age of 14 (way back in 1997), he fell in love with the web and has been tinkering with it ever since (more …)

Unless noted otherwise, the contents of this post are licensed under the Creative Commons Attribution 4.0 License and code samples are licensed under the MIT License

Join the Conversation

3 Comments

  1. Thank you for covering my polyfill in your blog article and giving it a wider reach.

    Hope you don’t mind when I bring up a few point:

    – Your first example of a _style query_ doesn’t need the outer parens `$someElement.matchContainer(“(style(–my-property: some-value))”)` (but I don’t know if it would still work, though)

    – The headline in the first demo reads “Style Query” but should probably be “Size Query”

    – The note about needing to register a custom property when it is used in a _style query_ might be based on a misunderstanding. The polyfill certainly doesn’t need it. So as long as your target browser would correctly match `@container style(–my-property: some-value)` when used purely in CSS you don’t need to register the property. Not sure if some browsers still have buggy support for _style queries_ in CSS and your note actually does refer to that fact.

  2. Thanks for the feedback, Martin!

    – The extra parens are not required indeed, but also do no harm. It’s an artifact of me copy-pasting from the size query example.

    – Uhoh! I’ve updated the title.

    – Yes, you’re right! The workaround I mentioned is only needed when observing the custom props themselves directly, not through a container query condition. I’ve removed the erroneous parts from the post.

    Thanks again for making `match-container` – I’m really digging it!

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.