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:
- Author the fallback as the default.
- Wrap the modern improvement in
@supports. - 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.canPlayTypeorImage()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. Thenotform 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
- CSS Conditional Rules Module Level 3, §5 @supports.
- The
@supports selector()proposal in Level 4 adds the selector test form. - Bramus’s
@supportsrecipes catalogue 12 worked patterns. - State of CSS 2024
reports about 65% of survey respondents use
@supportsin production.