Library / SDK
GoogleChromeLabs/container-query-polyfill avatar
GoogleChromeLabs/container-query-polyfill

container-query-polyfill: CSS container queries for browsers without them

A polyfill for CSS Container Queries

1,150 stars48 forksTypeScriptApache-2.0

At a glance

What is it?
The container-query-polyfill is a 9 kB polyfill for CSS container queries built on ResizeObserver and MutationObserver, supporting discrete and range queries plus the container relative length units. It has been in maintenance mode since November 2022, and its own documentation tells you to gate it behind a @supports check so the loading state is never shown on a fast connection.
Who is it for?
The polyfill fits a team maintaining a site for browsers that still lack native container query support, and it costs almost nothing in bytes, but it fits a project that has adopted container queries recently with some caution, because the package is in maintenance mode with no new features planned and the newest release is v1.0.2 from 2022-11-09.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 123 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A polyfill for a feature that shipped natively, still small enough to load anywhere

Container queries let a component respond to the size of its container rather than the size of the viewport, which is the change that makes component libraries work without a set of hard-coded breakpoints.

The polyfill is described as small, 9 kB compressed, and it implements the query syntax in full: discrete queries such as `width: 300` and `min-width: 300px`, range queries such as `200px < width < 400px` and `width < 400px`, and the container relative length units `cqw`, `cqh`, `cqi`, `cqb`, `cqmin` and `cqmax` in both properties and keyframes.

The mechanism is two standard observers. `ResizeObserver` is what notices that a container has changed size, and `MutationObserver` is what notices that the DOM or the styles under a container have changed. Together they are enough to re-evaluate the `@container` conditions whenever the answer could have changed, which is the whole job.

The stated browser support is Firefox 69 and later, Chrome 79 and later, Edge 79 and later, and Safari 13.4 and later. That list is about the observers and the CSS features the polyfill itself needs, not about where container queries work, since browsers that support the feature natively do not need the polyfill at all.

The project is Apache-2.0 licensed, published as `container-query-polyfill` on npm, and the repository has a `src/` directory, a `tests/` directory, a `CHANGELOG.md` and a `CONTRIBUTING.md`.

Maintenance mode is stated in the first line of the documentation

The most important fact about this project is in the very first line of the README, and it should shape any decision to adopt it.

The polyfill is in maintenance mode as of November 2022, and the project is not planning to add more features or enhancements. The last two releases reflect that: v1.0.2 on 2022-11-09 and v1.0.1 on 2022-10-19, so the numbering moved once in the middle of 2022 and then stopped. The last push to the repository was on 2026-06-22.

The reason for the freeze is not stated, and it is not hard to infer. Container queries are supported natively in every current browser, so a polyfill for them has a shrinking audience. The project was useful while native container query support was still spreading, and that window has closed.

What maintenance mode means for a user is precise. You are not getting new syntax support, so if the CSS spec adds a container query feature after 2022, the polyfill will not implement it. You are not getting a fix for the known limitations listed in the documentation unless someone contributes one, and the documentation actively invites contributions to the Shadow DOM and `calc(...)` gaps.

The corollary is that the value of this package now depends entirely on your browser support matrix. If you have a legacy browser in your support list, the polyfill is one of the few implementations available and its small size makes it a reasonable dependency. If you do not, it is dead weight and a maintenance liability, because a dependency that is never exercised on any real device is a dependency you will not notice breaking.

Install it as a package or a script tag, then gate it behind @supports

The installation is one npm command:

bash
npm install --save container-query-polyfill

or a script tag pointing at a CDN build:

js
<script src="https://cdn.jsdelivr.net/npm/container-query-polyfill@1/dist/container-query-polyfill.modern.js"></script>

The package manifest is worth a look for what it tells you about the shape of the library. It is an ES module, with `"type": "module"`, the module and unpkg entries both pointing at `dist/container-query-polyfill.modern.js`, and a single default export. The published files are the readme, the licence, the built JavaScript and the manifest itself, so nothing but the compiled bundle reaches npm. The build is done with microbundle, and the source of the package is `src/index.ts`.

The usage advice in the documentation is the part most people skip and should not. The recommendation is to use the polyfill initially only for content below the fold, and to use `@supports` queries to temporarily replace it with a loading indicator until the polyfill is ready to display it:

css
@supports not (container-type: inline-size) {
  .container,
  footer {
    display: none;
  }

  .loader {
    display: flex;
  }
}

The reasoning is straightforward. The polyfill has to do work before it can evaluate the queries, and until it has, the container-based layout is not applied. Hiding the affected content and showing a loader in the meantime avoids a flash of unstyled or wrongly sized content. The documentation notes that on sufficiently fast networks and devices, or on devices that natively support container queries, this loading indicator will never be displayed.

The trade is stated as well: the technique effectively limits the impact on FID and CLS, potentially at the expense of LCP, and you may see regressions in the latter, particularly on lower end devices or in poor network conditions. Hiding content to avoid a layout shift is a good trade against CLS and a bad one against how quickly the largest element appears.

What the polyfill does not do: inline styles, CSSOM, Shadow DOM, calc()

The limitations section is specific enough to decide adoption on its own, so it is worth taking each item seriously.

The first is CSS first. The polyfill only supports `<style>` elements and same-origin `<link>` elements. Inline styles via the `style` attribute and CSSOM methods are not polyfilled, and JavaScript APIs like `CSSContainerRule` are not polyfilled either, nor are APIs like `CSS.supports()` monkey-patched. So a build step that injects styles at runtime through the CSSOM, or a component that sets styles inline, gets no container query behaviour from the polyfill.

The second is best effort. Style changes that do not lead to observable DOM or layout mutations, for example changing `font-size` in a container without content, may not be detected, or may be detected a frame late on some browsers. Complex sibling CSS selectors are also not supported.

The third is the gap the project asks for help with. There is no support for Shadow DOM, and no support for functions like `calc(...)` in container conditions.

Those three together define the envelope. A project whose styles come from a stylesheet at build time, whose components do not use Shadow DOM, and whose container conditions do not use `calc()`, is in the supported envelope. A design system built on web components with shadow roots is not, at all, and there is no plan to change that.

One more note that saves an afternoon of confusion: the documentation warns that you may see errors like `ResizeObserver loop completed with undelivered notifications` or `ResizeObserver loop limit exceeded` when using the polyfill, and that these are expected and may safely be ignored. They are a consequence of the observer loop, not a misconfiguration on your side.

The :where() trick, and the dummy pseudo-class you may have to add by hand

There is one implementation detail with a visible consequence for your own CSS, and it is worth understanding because it explains an odd requirement.

The polyfill uses the CSS `:where()` pseudo-class to avoid changing the specificity of your rules. `:where()` has zero specificity by definition, so wrapping the selector in it means the rule's specificity is the same whether the polyfill is active or not. That matters during the load window and on browsers with native support, where the same stylesheet has to behave identically.

The catch is that `:where()` is relatively new, so a browser that lacks container queries may also lack `:where()`. For those browsers you have to append a dummy `:not(container-query-polyfill)` pseudo-class to the originating element of every selector under a `@container` block. The documentation shows the before and after side by side, and the difference for a simple selector is:

css
@container (min-width: 200px) {
  #foo:not(.container-query-polyfill) {
    /* ... */
  }
}

where the original was `#foo`. For a pseudo-element the placement changes, since it becomes `:not(.container-query-polyfill)::before`, and for a grouped selector every branch needs it.

The purpose is to make sure the specificity of your rules never changes, whether the polyfill is loading or the browser has native support. On browsers without `:where()` support, rules without the dummy are ignored.

This is the part of the polyfill that costs you real effort, because it means every `@container` block in your stylesheet has to be rewritten with a dummy class, and that has to be maintained by hand or by a build step. A project that only needs container queries in a few places can absorb that. A project that uses them everywhere will want a postcss transform, and the repository does not ship one.

The test suite runs the web platform tests through a browser

The build and test setup shows that the project took conformance seriously while it was being developed.

There are two build scripts. `build:wpt` compiles with `microbundle -f modern --no-compress` and defines `IS_WPT_BUILD=1`, and the ordinary `build` does the same with `--no-sourcemap` and `IS_WPT_BUILD=0`. That flag is what switches the bundle between the shipped version and the version used to run the web platform tests, since the test build needs to expose internals the shipped one should not.

The test command is `node --loader ts-node/esm ./tests/wpt.ts`, and the dependencies explain the rest. `selenium-webdriver` and `@types/selenium-webdriver` are the browser driver, `browserstack-local` allows the suite to run against real devices, and `async` is used to sequence the tests. So the suite drives an actual browser and runs the CSS Working Group's own test cases for container queries, which is a stronger claim than a set of hand-written assertions.

There is also a `serve` script, `superstatic -p 9606 .`, which serves the repository statically on port 9606, with a `superstatic.json` configuration in the root. That is the manual test harness for a page that loads the built polyfill.

The rest of the tooling is ordinary: eslint with a TypeScript parser and plugin, prettier with its own ignore file, rimraf for the clean step, and microbundle as the bundler. The author credit lists Google Chrome Developers with a contributor line for Surma, and the manifest is published with the source as `src/index.ts`.

Editorial conclusion

The polyfill fits a team maintaining a site for browsers that still lack native container query support, and it costs almost nothing in bytes, but it fits a project that has adopted container queries recently with some caution, because the package is in maintenance mode with no new features planned and the newest release is v1.0.2 from 2022-11-09. Before you add it, check whether your browser support matrix still needs it, and if it does, adopt the `@supports not (container-type: inline-size)` pattern the documentation recommends so the layout does not shift while the polyfill loads.

Frequently asked questions

How do I install the container-query-polyfill?

Run `npm install --save container-query-polyfill`, or load the CDN build with a script tag pointing at `https://cdn.jsdelivr.net/npm/container-query-polyfill@1/dist/container-query-polyfill.modern.js`. The package is an ES module with a single default export and the built bundle is 9 kB compressed.

Is the container-query-polyfill still maintained?

It is in maintenance mode as of November 2022, and no new features or enhancements are planned. The releases are v1.0.2 from 2022-11-09 and v1.0.1 from 2022-10-19, and the last push to the repository was on 2026-06-22.

What does the container-query-polyfill not support?

It only supports `<style>` and same-origin `<link>` elements, so inline styles and CSSOM methods are not polyfilled, nor are APIs like `CSSContainerRule` and `CSS.supports()`. There is also no Shadow DOM support and no support for `calc(...)` in container conditions.

Why do I need to add a dummy pseudo-class to my @container selectors?

The polyfill uses `:where()` to keep the specificity of your rules unchanged, but `:where()` is not available in the older browsers that need the polyfill. In those browsers you have to append `:not(.container-query-polyfill)` to the originating element of every selector inside a `@container` block.

Official sources

  1. GoogleChromeLabs/container-query-polyfill on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/googlechromelabs-container-query-polyfill.svg)](https://hysenlabs.com/projects/googlechromelabs-container-query-polyfill)