Container Query Polyfills: What They Cost and When to Skip Them

When container queries first shipped, polyfills filled the gap for browsers that had not caught up, and some teams still load one by habit. Every modern engine now supports container size queries natively, so the question has flipped: is the remaining audience of older browsers worth the cost a polyfill imposes on them — and, if loaded carelessly, on everyone else? The narrow problem this page addresses is making that decision on evidence: what a polyfill actually does at runtime, what each part costs, how to load it only where needed, and why an intrinsic CSS fallback is usually the better answer. It is part of Container Query Fallbacks in the Mastering Container Queries & Responsive Layouts guide.

Why polyfilling layout is expensive

Most polyfills add a function or a method — work that runs only when called. A container query polyfill emulates part of the layout engine, which runs continuously. It must discover every container, measure it whenever it might change size, evaluate every condition, and toggle styles before the user notices the wrong layout. None of that is free, and all of it happens on the main thread in the browsers least equipped to absorb extra work.

What a container query polyfill does at runtime Five stages: download and parse stylesheets, rewrite container rules into attribute selectors, attach ResizeObserver to each container, evaluate conditions on each resize, and set attributes that trigger ordinary CSS. Costs noted beneath: bytes, a flash of wrong layout before the first evaluation, and repeated layout work on every resize. Emulating layout in script, step by step parse CSS find @container rewrite rules to attributes observe ResizeObserver evaluate each condition set attrs CSS re-applies cost: bytes + parse time before anything renders correctly cost: layout work on every resize plus a flash of the unqueried layout on first paint Native container queries do all five inside the layout engine, in the same pass that lays the page out.

Three costs follow from that pipeline:

  1. Bytes and parse time. The polyfill must be downloaded and executed, and it must fetch or read every stylesheet to find @container rules. On slow devices and connections — typical of the older browsers that need it — this delays interactivity.
  2. A flash of the wrong layout. The browser paints the page before the polyfill has measured containers, so components first appear in their default, unqueried layout and then jump. That jump is a layout shift users see and metrics record.
  3. Continuous layout work. Every container resize fires an observer callback, which evaluates conditions and toggles attributes, which triggers a style recalculation and another layout. Native container queries fold this into a single layout pass; the polyfill does it in rounds.

What the user sees on a slow device

The flash of unqueried layout is easy to dismiss in a fast development machine, where the polyfill runs within a frame or two. On the older, slower devices that are the polyfill's only audience, the gap between first paint and the polyfill's first evaluation can be hundreds of milliseconds, and the page visibly rearranges.

First paint with a polyfill versus an intrinsic fallback Top timeline: the page paints with cards stacked, the polyfill downloads and runs, then cards jump into a two-column layout, producing a layout shift. Bottom timeline: an intrinsic layout paints once in its final arrangement and never moves. Load timeline on an older, slower device polyfill default layout download + evaluate queried layout layout shift intrinsic final layout from the first frame, adapting as space changes The intrinsic version may be less refined, but it never moves under the reader.

That jump is not only unpleasant; it can cause mis-clicks when a button the user was about to tap moves, and it registers in Cumulative Layout Shift, a Core Web Vitals metric. The cq-pending hiding technique below trades the jump for a brief blank, which avoids mis-clicks but delays reading. Neither is as good as a layout that is right on the first frame, which is the strongest argument for intrinsic fallbacks.

It is also worth being honest about coverage. Polyfills implement the common subset of container queries well, but newer capabilities — style queries, container query units in every context, containers inside shadow roots — may be partial or missing. A design that depends on those features gets an incomplete emulation in exactly the browsers least able to cope, so the fallback path needs designing anyway. Once that design exists, it is often good enough to ship on its own.

The complete implementation: load it only where needed

If the analytics show a meaningful audience on browsers without container queries and the design genuinely depends on them, the polyfill should be loaded conditionally so supporting browsers pay nothing. A small inline script tests support and injects the polyfill only when the test fails.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Conditional container query polyfill</title>
<script>
  // Supporting browsers (the vast majority) skip this entirely.
  if (!window.CSS || !CSS.supports('container-type: inline-size')) {
    const s = document.createElement('script');
    s.src = '/vendor/container-query-polyfill.js';   // self-hosted copy
    s.async = true;
    document.head.appendChild(s);
    // Mark the page so CSS can hide queried components until evaluated,
    // trading a brief blank for a visible jump in layout.
    document.documentElement.classList.add('cq-pending');
  }
</script>
<style>
  .card-slot { container-type: inline-size; }
  .card { display: grid; gap: 1rem; }

  @container (width > 30rem) {
    .card { grid-template-columns: 10rem 1fr; }
  }

  /* While the polyfill is working, hold queried components invisible to
     avoid the flash of the unqueried layout. Keep this short: the class
     should be removed once the polyfill reports it has run. */
  .cq-pending .card { visibility: hidden; }
</style>
</head>
<body>
  <div class="card-slot"><article class="card"><div>Media</div><div>Text</div></article></div>
</body>
</html>

The cq-pending class is a judgement call. Hiding components until the polyfill evaluates them avoids a visible jump, but it also means content is invisible for as long as the script takes, which on a slow device may be long. If the unqueried default layout is acceptable, skip the hiding and let the layout adjust. Either way, removing the class must happen in a callback the polyfill provides, and there must be a timeout that removes it regardless, so a failed script can never leave content hidden.

Self-host the polyfill rather than loading it from a third-party CDN. The script runs in exactly the browsers where failures are hardest to debug, and a CDN outage or blocked domain would then leave those users with hidden or broken components. Self-hosting also lets the file share the site's caching and security headers, and it removes a cross-origin dependency from the critical rendering path.

The key technique: an intrinsic fallback instead

The alternative is to accept that older browsers get a different, simpler layout — one built from intrinsic techniques that need no queries at all — and let container queries enhance it where supported. For most components this produces a better experience in the old browsers than a polyfill does, because it is instant and stable.

Polyfill or fallback? If an intrinsic layout such as flex-wrap or auto-fit grid is acceptable in old browsers, use it. If not, and the component sits in a predictable width, use a viewport media query fallback. Only if neither works and old-browser traffic is significant, load a polyfill conditionally. Reach for the polyfill last Is an intrinsic layout acceptable? yes no Intrinsic fallback flex-wrap, auto-fit, clamp() Predictable width per page? yes no Viewport fallback @media in @supports not Polyfill loaded conditionally Most components stop at the first box.

Intrinsic layouts such as breakpoint-free rows with flex-wrap and auto-fit grids already respond to available space without any query; the container query then only refines typography or spacing on top. The full set of intrinsic baselines is laid out in Intrinsic Layout Fallbacks Without Queries.

Variation: measuring whether the decision was right

A polyfill decision should be revisited as the audience changes, and the data to do so is easy to collect. Record, for a sample of sessions, whether CSS.supports('container-type: inline-size') is true, alongside a metric such as Cumulative Layout Shift. Two numbers answer the question: the share of sessions without support, and the layout shift those sessions experience with the current approach.

// Minimal, privacy-friendly telemetry: one boolean per session.
const supportsCQ = !!(window.CSS && CSS.supports('container-type: inline-size'));
navigator.sendBeacon?.('/metrics', JSON.stringify({ supportsCQ }));

When the unsupported share falls below the threshold your team treats as negligible, remove the polyfill and its loader — dead fallback code is still code that must be maintained, reviewed and tested on every release. The testing side of that maintenance is covered in Testing Fallbacks Without Old Browsers.

Browser support

Native container size queries are supported in Chrome and Edge 105+, Firefox 110+ and Safari 16+, and container query units in the same versions. CSS.supports() and @supports are supported in every browser that could plausibly load a polyfill; @supports selector() is supported in Chrome and Edge 83+, Firefox 69+ and Safari 14.1+. ResizeObserver, which polyfills depend on, is supported in all current engines and in most of the older versions a polyfill would target.

FAQ

Do I still need a container query polyfill? Rarely. Container size queries have been supported in Chrome and Edge since 105, Firefox since 110 and Safari since 16, so the browsers that need a polyfill are several years old. Most sites serve those browsers a simpler intrinsic or viewport-based layout instead.

How does a container query polyfill work? It parses stylesheets for @container rules, rewrites them into selectors that depend on attributes or classes, observes the size of each container with ResizeObserver, and sets those attributes when a condition matches. The browser then applies ordinary CSS rules.

What are the costs of a polyfill? Extra JavaScript to download and run, a flash of the unqueried layout before the script evaluates sizes, layout work on every resize as observers fire, and limited support for newer features such as style queries and container query units in all contexts.

Can a polyfill be loaded only for browsers that need it? Yes. Test support with CSS.supports('container-type: inline-size') or the @supports equivalent, and inject the polyfill script only when the test fails. Supporting browsers then pay no download or runtime cost.

Related articles

More pages in the same section.