Safari 17.0 shipped with a @container implementation that silently drops style updates when a container’s inline size changes via a transform rather than a layout reflow. The symptom in our design system was specific and maddening: card components inside a modal would render correctly on first paint, then refuse to re-evaluate their query conditions when the modal animated open with transform: scale(). The same markup worked perfectly in Safari 17.4 and in every Chromium build we tested. If you have been chasing a container queries safari bug that only reproduces under animation or in off-screen ancestors, the root cause is almost certainly one of three WebKit regressions that landed between Safari 17.0 and 17.3.

I want to walk through what actually breaks, which WebKit bug reports map to which symptom, and the workarounds that held up in production once we stopped trying to patch around them at the component level. Everything here assumes you are using container-type: inline-size — the size containment variant has its own separate set of problems that deserve their own write-up.

The exact failure mode in Safari 17.0 through 17.2

Our design system uses a Card primitive that changes its internal layout at three breakpoints: below 320px it stacks vertically, between 320 and 560 it uses a two-column grid, and above 560 it switches to a horizontal media-object layout with the thumbnail on the left. The CSS looked roughly like this:

.card-wrapper {
  container-type: inline-size;
  container-name: card;
}

@container card (min-width: 320px) {
  .card { display: grid; grid-template-columns: 1fr 1fr; }
}

@container card (min-width: 560px) {
  .card {
    grid-template-columns: 200px 1fr;
    grid-template-areas: "media body";
  }
}

In Chrome, Firefox, and Safari 17.4+, this behaves exactly as the spec describes. In Safari 17.0, 17.1, and 17.2, it breaks in two specific scenarios. The first: when the .card-wrapper lives inside an element that has transform, filter, or will-change applied. The second: when the wrapper is briefly display: none and then unhidden via a class toggle. Both scenarios leave the card permanently stuck in whatever breakpoint was active on its last successful layout pass. Resizing the window forces an update. Scrolling does not. A requestAnimationFrame reflow does not. Only an actual viewport-size change unsticks it.

The thing that made this hard to track down was that DevTools lied to us. Safari’s Web Inspector would show the container as being 600px wide, and it would show the @container card (min-width: 560px) rule as matching, but the computed styles on .card would reflect the 320px breakpoint. The discrepancy between “rule matches” and “rule applies” is what tipped me off that the bug was in the style invalidation path rather than in the query evaluation itself.

Why transformed ancestors poison container size resolution

The underlying issue is that WebKit’s layout engine treats containers with transformed ancestors as having an indeterminate inline size during certain reflow phases, then caches that indeterminate value. The CSS Containment Module Level 3 specification is explicit that container size queries resolve against the used inline size of the query container after layout, not against any transformed bounding box. Safari 17.0’s implementation was reading the post-transform box in a subset of cases, producing either a zero-width container or a stale cached width from the previous layout.

You can verify this yourself with a short reproduction. Wrap a container-typed element in a div that toggles transform: scale(1) on and off via a class. Every toggle should be a no-op — scale(1) is an identity transform — but in the affected Safari versions, the inner container’s @container rules stop matching on the second toggle.

<div class="stage">
  <div class="wrapper">
    <div class="card">content</div>
  </div>
</div>

<style>
  .stage { transform: scale(1); }
  .wrapper { container-type: inline-size; width: 600px; }
  .card { background: red; }
  @container (min-width: 560px) { .card { background: green; } }
</style>

On Safari 17.4 and up, .card is green. On 17.0 through 17.2 it is red roughly half the time, depending on how the element was inserted into the document. Freshly-parsed HTML tends to render correctly on first paint; anything added via appendChild after the parent has already been laid out will hit the bug.

Benchmark: Container Query Layout Perf: Safari 17
Performance comparison — Container Query Layout Perf: Safari 17.

Which WebKit bugs map to which symptom

Three separate WebKit bug reports cover the cluster of regressions I have been calling “the container queries safari bug.” They were filed between September 2023 and January 2024 and all landed fixes in Safari 17.4. It is worth understanding which one you are hitting because the workarounds differ.

The first and most common is the transformed ancestor invalidation bug. Containers inside any element with a non-identity (or identity!) transform fail to invalidate their size queries when the ancestor’s computed style changes. This is the one that broke our modal animations. The fix was to mark any element applying a transform as implicitly size-containing for the purpose of the invalidation walk — you can find the patch in the WebCore style engine source, specifically in the changes to ContainerQueryEvaluator::resolveContainer.

The second is the display-none toggle bug. A container that was display: none at the time its parent was laid out never registers itself with the container query scope. When it becomes visible, its size is measured correctly but none of the @container rules are evaluated for any descendant. This one is particularly nasty because the symptom depends on insertion order — if the container exists in the DOM at load time, you never hit it.

The third is the nested container cascade bug, where a container inside another container only evaluates its query against the outer container’s size, not its own. This was actually a spec-compliance bug rather than an invalidation bug, and the fix required reworking how container-name lookups resolve in the presence of shadow DOM boundaries.

The workarounds that actually held up

We tried four workarounds before settling on one. The first three all failed in subtle ways.

Adding contain: layout to the wrapper seemed to fix it, but only because it forced a different layout path that happened to not hit the bug on our test cases. On pages with lots of nested containers, contain: layout introduced a different set of scrolling issues because layout containment creates a new block formatting context, and the containment side effects were worse than the original bug.

Forcing a reflow with void element.offsetWidth after unhiding the container worked on Safari 17.1 but not on 17.0, because the cached container width was stored at a different pipeline stage. Any JS-based “kick the layout” workaround will be fragile across the 17.0/17.1/17.2 point releases, and I do not recommend going down that path.

Swapping transform: scale animations for width animations avoided the transformed ancestor path but tanked animation performance, because width changes cannot be GPU-composited. For a modal that animates in on every route change, this was not acceptable.

What actually worked was gating the container query behavior with a feature detection that specifically checks for the bug, not just for the presence of @container support. Safari 17.0 reports full container query support in CSS.supports("container-type: inline-size"), so the standard feature detection is useless. Instead, we run a one-time measurement at app startup that creates a 600px container inside a transformed ancestor and checks whether a @container (min-width: 560px) rule matches. If it does not, we set html[data-cq-broken="true"] and fall back to viewport-based media queries for affected components.

function detectContainerQueryBug() {
  const probe = document.createElement("div");
  probe.innerHTML = `
    <div style="transform: scale(1); position: absolute; left: -9999px;">
      <div style="container-type: inline-size; width: 600px;">
        <div id="cq-probe"></div>
      </div>
    </div>`;
  const style = document.createElement("style");
  style.textContent = `
    @container (min-width: 560px) {
      #cq-probe { --cq-works: 1; }
    }`;
  document.head.appendChild(style);
  document.body.appendChild(probe);
  const works = getComputedStyle(
    probe.querySelector("#cq-probe")
  ).getPropertyValue("--cq-works").trim() === "1";
  probe.remove();
  style.remove();
  if (!works) document.documentElement.dataset.cqBroken = "true";
}

This runs in about 2ms on a mid-tier laptop and is the only detection I trust across the affected Safari range. It has the added benefit of naturally expiring itself once users upgrade to 17.4 — when the bug is fixed, the probe reports working and no fallback class gets applied.

Mapping the fallback in your design system

The fallback itself is where design system work gets tedious. We rewrote the breakpoints as an attribute-gated variant: the Card component checks for html[data-cq-broken="true"] and swaps in a media-query-based implementation that uses the viewport width as a proxy. This is worse than true container queries — a card inside a sidebar will now reflow based on the whole window rather than its actual column — but it is deterministic and matches how the same component behaved before we migrated to container queries in the first place.

.card-wrapper { container-type: inline-size; }
@container (min-width: 560px) {
  .card { grid-template-columns: 200px 1fr; }
}

html[data-cq-broken="true"] .card-wrapper { container-type: normal; }
@media (min-width: 900px) {
  html[data-cq-broken="true"] .card { grid-template-columns: 200px 1fr; }
}

The trick here is resetting container-type to normal on the broken-Safari branch. If you leave it as inline-size, the container is still registered and Safari still tries to evaluate the @container rule, which means the bug can still fire and override your media-query fallback depending on which rule cascades last. Explicitly clearing the container type forces Safari to treat the element as a regular block and the media query becomes the only source of truth.

Official documentation for container queries safari bug
Official documentation — the primary source for this topic.

A few smaller notes from the migration that are worth keeping in mind. The WebKit blog’s original container queries announcement describes the initial implementation as shipping with Safari 16, but several invalidation paths were rewritten between 16.4 and 17.0, and the bugs I am describing here were introduced during that rewrite. If you are supporting Safari 16 users, they are actually safer than 17.0 users for this particular feature. The MDN reference for container size and style queries documents the behavior that 17.4+ complies with and is a reasonable source of truth for what the feature should do on any browser.

Style queries have a related but separate failure

While tracking down the size query issues, I ran into a second problem that is worth calling out because it looks identical in the DevTools inspector. Style queries — @container style(--theme: dark) — do not trigger on custom property changes that are propagated from an ancestor inheritance, only on direct assignments to the container element itself. This is not a Safari-specific bug (it affects Chrome as well, and it may actually be spec-intended), but it will bite you if you assume style queries work symmetrically with size queries.

The practical consequence is that if you use a CSS variable on the :root to control a theme, then query that variable inside a nested container, the query will fire once at first paint and never again. The workaround is to assign the custom property to the container element directly, either with JavaScript or with a wrapper rule. It is ugly, and I have not found a clean pattern for it yet, but the alternative of using data attributes for theming works fine and sidesteps the issue entirely.

When to drop the fallback

As of April 2026, Safari 17.0 through 17.2 represent a small but non-trivial slice of Apple platform traffic — mostly iPhone users on devices that do not qualify for the current iOS major, or macOS Ventura users who pinned to Safari 17.2 to avoid the 17.3 autofill regression. If your analytics show you below 0.5% traffic from those versions, the detection script is cheap enough to keep running anyway; the cost is a single synchronous layout at app startup. If you are above 2%, the fallback branch is still worth maintaining.

Delete the fallback when your Safari 17.0-17.2 share drops under your design system’s general legacy-browser threshold — whatever that is for your project. Keep the detection script in place for one more release cycle after that, because removing it is reversible and removing the fallback CSS is the kind of change that tends to break subtly on pages you forgot you shipped. The bug is fixed upstream; the long tail of users on affected Safari builds is the only thing you are still working around.

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

Zeen Social Icons