CSS @supports: Feature Queries Explained
CSS @supports lets a stylesheet test whether a browser supports a property or selector before using it. How feature queries work and when to reach for them.
@supports is a CSS at-rule that lets a stylesheet test whether the browser understands a given property, value, or selector, and apply a block of rules only when the test passes. It’s CSS’s own built-in feature detection — no JavaScript required — and it’s what makes it safe to ship cutting-edge CSS features to every browser at once, with a plain fallback for the ones that don’t support them yet.
The basic syntax
A feature query looks like a media query but tests CSS support instead of viewport size:
@supports (backdrop-filter: blur(10px)) {
.modal-backdrop {
backdrop-filter: blur(10px);
}
}
The browser parses the condition — (backdrop-filter: blur(10px)) — and checks whether it recognizes that property and can accept that value. If it can, everything inside the block applies. If it can’t, the block is skipped entirely, as if it were never written. This is different from just declaring the property directly: an unsupported property in a normal rule is silently ignored by browsers, which is often fine, but doesn’t let you provide a deliberate alternative for browsers that lack it.
Testing selectors, not just properties
@supports can also test selector support using the selector() function, which matters more since the arrival of newer selectors like :has():
@supports selector(:has(*)) {
.card:has(img) {
grid-template-columns: 1fr 2fr;
}
}
Without this check, a browser that doesn’t understand :has() wouldn’t throw an error — but an entire selector list containing an unrecognized pseudo-class gets dropped by some parsers, which can silently break unrelated rules bundled in the same selector. Wrapping the modern selector in a selector() feature query keeps the fallback layout intact for older browsers while still shipping the enhanced version everywhere it’s supported.
Combining conditions
Feature queries support and, or, and not, so you can test for combinations:
@supports (display: grid) and (gap: 1rem) {
.layout {
display: grid;
gap: 1rem;
}
}
@supports not (aspect-ratio: 1) {
.thumbnail {
padding-top: 100%; /* legacy aspect-ratio hack */
}
}
The not form is the most common pattern for progressive enhancement: write the modern approach as your default CSS, then use @supports not (...) to layer in a fallback specifically for browsers that lack the feature, rather than writing the fallback as the default and overriding it everywhere else.
Where it fits in the cascade
@supports blocks participate in the cascade exactly like any other rule at the position where they appear — they don’t automatically win or lose against other selectors based on specificity of the condition itself. This is a common point of confusion with cascade layers, which control precedence between whole groups of rules; @supports only controls whether a block of rules is parsed and applied at all, not how it’s weighted against other rules once it is. The two features solve different problems and combine cleanly — you can nest a cascade layer inside a feature query or vice versa.
It’s also worth distinguishing @supports from container queries and standard media queries: those all conditionally apply styles, but on different axes. A media query checks viewport or device characteristics; a container query checks an element’s own container size; @supports checks browser capability. They’re frequently used together — for instance, gating a container-query-based layout behind a feature query so browsers without container query support fall back to a simpler grid.
Practical patterns
A few places @supports earns its keep in real stylesheets:
- Guarding brand-new properties. When adopting something recently added to browsers, wrap the modern declaration in a feature query and put a safe, older equivalent outside it (or behind
@supports not). - Selector-dependent layout changes. Anything gated on
:has(),:is(), or other relatively new selectors benefits fromselector()checks, especially if the fallback layout needs to look meaningfully different rather than just “unstyled.” - JS-free progressive enhancement. Where a JavaScript feature-detection library would otherwise be pulled in just to toggle a class,
@supportsfrequently replaces that need entirely for purely visual features, keeping the enhancement declarative and framework-agnostic. This overlaps in spirit with the broader idea of progressive enhancement: ship a working baseline, then layer on capability where it’s detected.
A caution on writing conditions
The condition inside @supports must be valid CSS syntax on its own — you can’t write a bare property name like @supports (gap); it has to be a property-value pair, @supports (gap: 1rem). A common mistake is assuming support for one value of a property implies support for all its values; browsers can support a property for some value types before others, so testing with the specific value you intend to use is safer than testing a generic placeholder value.
The takeaway
@supports is a native, JavaScript-free way to write CSS that adapts to what the browser can actually do, testing either a property-value pair or, with selector(), an entire selector. It doesn’t affect specificity or cascade order on its own — it only decides whether a block of rules is applied at all — which makes it a clean tool for progressive enhancement: ship the modern approach as the default, and use @supports not to carve out a deliberate fallback rather than hoping an unsupported property fails silently in a way that still looks acceptable.
Tagged
Keep reading
Takina · · 4 min read CSS @starting-style Explained: Entry Transitions in CSS
CSS @starting-style defines an element's before-state on first render, so transitions can animate elements as they appear, even from display: none.
Takina · · 4 min read Container Query Units: cqw, cqh, and cqi Explained
Container query units like cqw and cqi size elements relative to their containing element, not the viewport — here's how and when to use them.
Takina · · 4 min read CSS Relative Color Syntax: Deriving Colors from Colors
Relative color syntax lets CSS derive a new color from an existing one by adjusting its channels directly, no preprocessor or JS required.