Home CSS

CSS @supports — feature queries

@supports tests for CSS feature support at parse time, letting authors ship modern CSS to capable engines and a tested fallback to older ones. Replaces the @media (-webkit-...) hacks of 2010-era CSS.

What @supports does

@supports is a conditional at-rule whose body applies only when the user agent claims to support the test condition. The condition syntax mirrors CSS declarations and selectors:

@supports (display: grid) {
  .layout { display: grid; }
}

@supports selector(:has(*)) {
  body:has(dialog[open]) { overflow: hidden; }
}

@supports (color: oklch(50% 0.1 200)) {
  :root { --c-brand: oklch(54% 0.18 295); }
}

The test parses each declaration; if the engine accepts the declaration without erroring, the condition is true. The same applies to selector() queries — if the selector is parseable as the engine understands it, the condition matches.

When to use it

@supports is appropriate when an experimental or recently-shipped feature has a meaningful fallback. The pattern:

  1. Author the fallback as the default.
  2. Wrap the modern improvement in @supports.
  3. The two paths produce visually equivalent UIs; the modern path is simply nicer or more performant.
.gallery {
  display: flex;          /* fallback for engines without grid */
  flex-wrap: wrap;
}
@supports (display: grid) {
  .gallery {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: var(--space-3);
  }
}

Most production codebases stopped wrapping display: grid in @supports around 2018, when interop reached 95%. Today the canonical use cases are:

  • @supports selector(:has(*)) — interop reached 92% in 2023.
  • @supports (animation-timeline: scroll()) — for scroll-driven animations, partial as of 2024.
  • @supports (color: color(display-p3 1 0 0)) — for wide-gamut colour selection.
  • @supports (anchor-name: --a) — for anchor positioning, a 2024 feature that ships unevenly.

Composing conditions

@supports (display: grid) and (gap: 1rem) { … }
@supports (display: grid) or (display: flex) { … }
@supports not (-webkit-overflow-scrolling: touch) { … }
@supports (display: grid) and ((color: oklch(0% 0 0)) or (color: lch(0% 0 0))) { … }

and, or, and not are the only allowed combinators. Parentheses group conditions.

What @supports cannot test

@supports parses syntax; it does not test runtime behaviour. The following are not detectable via @supports:

  • Whether a specific value of a property has the desired behaviour (@supports (display: contents) is true everywhere; the bug is in the AX tree exposure, not the parse).
  • Whether an animation runs smoothly.
  • Whether a colour gamut is actually displayable on the user’s monitor (use @media (color-gamut: p3) for that).
  • Whether a media format decodes (use HTMLImageElement.canPlayType or Image() constructor).

The boundary is sometimes fuzzy: feature-query for selector(:has(*)) returns true once the parser recognises the syntax, even before the engine fully implements the matching algorithm. Most cases this is harmless; for brand-new features, test in real browsers in addition to the feature query.

Cross-engine support

@supports itself reached cross-engine interop in 2017 across Chromium, WebKit, and Gecko. The selector() function reached interop in 2022. The font-tech() and font-format() functions reached interop in 2023.

caniuse for @supports reports about 99% global support; pages that need to support Internet Explorer 11 still cannot use @supports and must rely on JavaScript-based feature detection.

Common patterns

Progressive colour spaces

:root { --brand: #7a4fcf; }
@supports (color: oklch(0% 0 0)) {
  :root { --brand: oklch(54% 0.18 295); }
}

Conditional :has()

.field { border: 1px solid var(--c-border); }
.field:has(:invalid) { border-color: var(--c-error); }

/* Fallback for older engines: rely on a class set by JS */
@supports not selector(:has(*)) {
  .field--invalid { border-color: var(--c-error); }
}

Anchor positioning with fallback to position: fixed

.popover {
  position: fixed;
  inset: 50% auto auto 50%;
}
@supports (anchor-name: --x) {
  .popover {
    position: absolute;
    inset: auto;
    anchor-name: --p;
    position-anchor: --p;
    inset-area: bottom;
  }
}

Common pitfalls

  • Wrapping a long-supported feature. Adds parse cost and visual clutter for no benefit. Drop the wrapper once interop reaches 95% — track on caniuse.
  • Negation that backfires. @supports not (display: grid) is equivalent to “old browsers” — but new browsers without grid support are rare. The not form mostly catches edge-case WebViews.
  • Selector inside the condition without selector(). Plain @supports (:has(...)) is a parse error; use @supports selector(:has(*)).
  • Property-only check for a value-specific feature. @supports (color: red) is true everywhere; the test is meaningless. Test the value you actually use.

Further reading