Feature Detection with @supports: Container Queries plus a Viewport Fallback

Problem statement

You want to ship a component that uses container queries today but still renders a sensible layout in an engine that does not support them, without maintaining two separate stylesheets or shipping a heavy polyfill. The naive approach — writing @container rules and hoping older browsers ignore them — half works, because unsupported browsers do skip the @container block, but it leaves those users with only the unstyled baseline and no responsive behavior at all. The clean solution is to feature-detect with @supports (container-type: inline-size), route supporting browsers down the container path, and give everyone else a viewport-based @media fallback. This guide belongs to Container Query Fallbacks, part of Mastering Container Queries & Responsive Layouts.

Approach rationale

@supports evaluates a property/value pair and applies its block only when the browser considers that pair valid. Testing container-type: inline-size is the canonical probe because container-type is the gateway property for size queries: any engine that understands it also understands @container. The reason to detect the property rather than the at-rule is that @supports has no portable, reliably-implemented syntax for testing at-rule support across all the engines you care about, whereas the property test has worked since @supports itself shipped.

The alternative strategies are worse for different reasons. A JavaScript polyfill that watches every container with ResizeObserver adds main-thread work and a script dependency, and it can produce a visible reflow as classes are applied after first paint — a real accessibility problem for users on slow hardware who see content jump. Doing nothing and relying on the implicit skip leaves legacy users with a broken layout. The @supports + @media pairing keeps everything in CSS, costs nothing at runtime, and degrades to a perfectly usable viewport-driven layout. Its only tradeoff is that the fallback uses the viewport rather than the component's own width, so a component in a narrow sidebar on a wide screen falls back to its wide layout — usually acceptable, and the price of zero JavaScript.

Complete working implementation

Feature detection decision flow Flow from an @supports test to either the modern container query path or the @media viewport fallback path. Routing support detection to two layout paths @supports (container-type: inline-size) true false Modern path @container (min-width) Viewport fallback @media (min-width) shared mobile-first baseline
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
  /* 1. Shared mobile-first baseline. EVERY browser gets this. */
  .panel {
    display: grid;
    grid-template-columns: 1fr; /* stacked by default */
    gap: 1rem;
    padding: 1rem;
    background: #11141c;
    color: #e8ecf4;
    border-radius: 12px;
  }

  /* 2. Viewport fallback for engines WITHOUT container queries.            */
  /*    Comes first so supporting browsers can override it on equal weight. */
  @media (min-width: 720px) {
    .panel {
      grid-template-columns: 200px 1fr; /* media beside body on wide viewports */
      align-items: start;
    }
  }

  /* 3. Modern path, gated by feature detection. Skipped entirely by        */
  /*    browsers that do not understand container-type: inline-size.        */
  @supports (container-type: inline-size) {
    .panel-wrap {
      container: panel / inline-size;
    }

    /* Reset the viewport fallback so it cannot fight the container query.   */
    .panel {
      grid-template-columns: 1fr;
    }

    /* Now react to the COMPONENT's width, not the viewport's. */
    @container panel (min-width: 460px) {
      .panel {
        grid-template-columns: 200px 1fr;
        align-items: start;
      }
    }
  }

  .panel img {
    width: 100%;
    border-radius: 8px;
    aspect-ratio: 4 / 3;
    object-fit: cover;
  }
</style>
</head>
<body>
  <div class="panel-wrap">
    <section class="panel">
      <img src="https://placehold.co/200x150" alt="Illustration">
      <div>
        <h2>Progressive panel</h2>
        <p>Container-query browsers lay this out by the panel's own width; older
           browsers fall back to a viewport breakpoint. Both render usefully.</p>
      </div>
    </section>
  </div>
</body>
</html>

Key technique callout

The pivotal detail is the ordering and the reset inside the @supports block. A browser that supports container queries reads all three layers: it applies the baseline, then the @media fallback (because that media query may still match the viewport), and finally the @supports block — which must explicitly set grid-template-columns: 1fr again to undo the @media rule before the @container query takes over. Without that reset, on a wide screen the @media (min-width: 720px) rule would already have applied the two-column layout, and a narrow container's @container rule could not pull it back, because the two rules have equal specificity and the media one is sometimes the later match depending on source order. By resetting to the stacked baseline at the top of the @supports block, you guarantee the container query is the sole authority on layout for supporting browsers, while unsupported browsers — which never enter the block — keep the viewport behavior untouched.

Variation or extension

You can probe a polyfill load in JavaScript with the matching CSS.supports() call, and pair the whole pattern with prefers-reduced-motion so any transition introduced by the layout swap is suppressed for users who ask for less motion. This connects the responsive layer to the accessibility motion guidance for animations.

/* Animate the layout swap, but only when the user has not requested reduced motion */
@supports (container-type: inline-size) {
  @media (prefers-reduced-motion: no-preference) {
    .panel {
      transition: grid-template-columns 0.2s ease;
    }
  }
}
// Conditionally load a polyfill only where support is missing.
if (!CSS.supports('container-type', 'inline-size')) {
  import('https://cdn.example.com/container-query-polyfill.modern.js');
}

Browser support note

@supports itself is universally available (Chrome and Edge 28+, Safari 9+, Firefox 22+), so the detection wrapper carries no risk. Size container queries — the feature being detected — are baseline in Chrome and Edge 105+, Safari 16.0+, and Firefox 110+. The CSS.supports() JavaScript API has matching reach. Because every browser that lacks container queries skips the @supports block automatically, this pattern needs no further guard, which is why it pairs naturally with the broader strategies in the guide to handling container query fallbacks for older browsers.

FAQ

Why test container-type instead of @container directly?@supports cannot test an at-rule's existence reliably across engines, but it can test whether a property and value pair is valid. @supports (container-type: inline-size) is the canonical, widely supported probe for size container query support.

Will browsers that lack container queries apply rules inside @supports? No. A browser that does not recognise container-type: inline-size as valid evaluates the @supports condition as false and skips the entire block, so the modern path is invisible to it and your fallback styles take over.

Should the viewport fallback come before or after the @supports block? Put the mobile-first baseline and the @media viewport fallback first, then the @supports block last. Supporting browsers read both but the later container rules win on equal specificity; unsupported browsers only ever see the baseline and media query.

Can I detect container query support in JavaScript? Yes, with CSS.supports('container-type', 'inline-size'), which returns a boolean. This is useful for conditionally loading a polyfill, but for styling alone the CSS @supports rule is sufficient and avoids a flash of unstyled layout.

Related articles

More pages in the same section.