CSS Feature Queries (@supports)
What You Learn From This Page
- How
@supportstests CSS declarations and conditionally adds rules to the cascade. - How to design a usable fallback before applying progressive enhancements.
- How
not,and,or, and parentheses form support conditions. - How
selector(),font-tech(), andfont-format()checks differ from declaration tests. - How to upgrade Grid, subgrid, selectors, colors, viewport units, and container behavior safely.
- What feature queries cannot prove about quality, bugs, performance, HTML, or JavaScript APIs.
- How browser support policy determines whether a fallback or query is necessary.
- How to organize, debug, test, and review feature queries in production CSS.
Introduction
CSS feature queries let a stylesheet ask whether the browser supports a particular CSS capability. The @supports rule usually tests a property and value, such as display: grid, then conditionally adds a block of rules to the cascade. This makes it possible to begin with a broadly usable baseline and upgrade the design when a newer feature is available.
The central benefit is feature detection rather than browser detection. Code does not need to guess that one browser version supports Grid while another does not. It asks the browser to evaluate the syntax it intends to use. As implementations evolve, the condition continues to describe capability instead of relying on a name or user-agent string.
A successful feature-query strategy does more than wrap modern declarations. It decides whether a fallback is required, keeps essential content usable outside the enhancement, and understands the limits of the test. Recognition of syntax does not guarantee perfect behavior, visual consistency, or good performance. Real browser testing and a documented support policy remain necessary.
What Is @supports?
@supports is a CSS conditional group rule. When its condition evaluates true, the rules inside become eligible for the normal cascade. When false, the browser ignores that block.
@supports (display: grid) {
.layout {
display: grid;
}
}
The query asks whether the declaration display: grid is supported. It does not create Grid by itself; the enclosed declaration performs the actual layout change.
Basic Declaration Syntax
A declaration condition contains a property, a colon, and a value inside parentheses.
@supports (property: value) {
/* Enhanced CSS rules */
}
Write the actual syntax that the enhancement requires. @supports (display) is not a declaration test, and omitting the parentheses produces invalid feature-query syntax.
What a Declaration Test Proves
A successful test means the browser parses the declaration as supported according to its implementation. For example, @supports (display: grid) establishes that the browser recognizes that value for display. It is a capability gate, not a certification of every Grid edge case.
The browser may still contain implementation bugs, rendering differences, incomplete interactions with other features, or performance limitations. Critical functionality requires testing beyond the Boolean result.
Progressive Enhancement Starts with a Baseline
The strongest pattern places a complete fallback outside the query and applies an upgrade inside it.
.layout {
display: block;
}
@supports (display: grid) {
.layout {
display: grid;
grid-template-columns: 16rem minmax(0, 1fr);
}
}
A browser without Grid receives normal block flow. A supporting browser receives the richer two-column relationship. Both preserve content and task order.
Why the Fallback Usually Comes First
A browser too old to understand @supports ignores the entire unknown at-rule block. If the only useful layout lives inside that block, the baseline may be unstyled or broken. Rules outside the query provide protection even when the conditional mechanism itself is unavailable.
The fallback does not need to reproduce the enhancement exactly. Stacked cards can be an acceptable alternative to an aligned responsive grid. Progressive enhancement seeks usability and meaning, not pixel-identical rendering.
Use not to Test Lack of Support
The not operator negates a support condition.
@supports not (backdrop-filter: blur(10px)) {
.panel {
background: rgb(255 255 255 / 0.96);
}
}
This is valid, but a positive enhancement is often easier to maintain: define the solid readable background as the base, then introduce translucency and blur when supported. Use negative conditions when the unsupported case genuinely needs rules that cannot serve as the baseline.
Use and When Every Feature Is Required
The and operator requires both sides to match.
@supports
(display: grid)
and (grid-template-columns: subgrid) {
.form-row {
display: grid;
grid-template-columns: subgrid;
}
}
Test all capabilities that the coordinated enhancement actually uses. Avoid adding unrelated checks merely to identify a browser family.
Use or for Supported Alternatives
The or operator matches when at least one alternative is supported.
@supports
(backdrop-filter: blur(10px))
or (-webkit-backdrop-filter: blur(10px)) {
.panel {
background: rgb(255 255 255 / 0.72);
}
}
If the enclosed implementation differs by alternative, a single shared block may not be enough. Only include vendor-prefixed forms when current support targets and testing justify them.
Use Parentheses to Clarify Logic
Complex conditions should group related expressions explicitly.
@supports
((display: grid) and (gap: 1rem))
or (display: flex) {
.layout {
/* Enhancement supported by either path */
}
}
Readable logic is more important than showing every operator in one condition. If a query takes substantial effort to interpret, separate enhancements or simplify the compatibility plan.
Selector Support with selector()
Declaration syntax cannot test whether a selector is available. The selector() function checks selector parsing.
@supports selector(:has(*)) {
.card:has(img) {
padding-block-start: 0;
}
}
The argument should reflect the selector capability needed by the enhancement. Keep a usable style for elements that never receive the conditional selector rule.
Testing :has()
The relational :has() pseudo-class can style an element based on related descendants or siblings. A fallback should not depend on that parent-state styling for essential validation.
.field {
border: 1px solid #6b7280;
}
@supports selector(.field:has(input:invalid)) {
.field:has(input:invalid) {
border-color: #b42318;
}
}
The input should still expose semantic validity, an associated message, and accessible state. The enhanced parent border is supplemental feedback.
Testing :focus-visible Safely
A baseline focus indicator is essential. Supporting browsers can refine when it appears through :focus-visible.
button:focus {
outline: 2px solid currentColor;
outline-offset: 3px;
}
@supports selector(button:focus-visible) {
button:focus:not(:focus-visible) {
outline: none;
}
button:focus-visible {
outline: 2px solid currentColor;
}
}
Do not remove the fallback outline before confirming the enhanced selector is available. Keyboard focus must remain visible in every supported browser.
Testing Modern Selector Groups
selector() can also gate :is(), :where(), or newer selector forms when a coordinated fallback is required.
@supports selector(:where(h1, h2)) {
:where(h1, h2) {
line-height: 1.2;
}
}
Simple selector rules may already fail safely if ignored. A query adds value when several declarations or fallback selectors must change together, not merely because a feature is modern.
Testing CSS Nesting
Native nesting can be tested through selector syntax such as the nesting selector.
@supports selector(&) {
.card {
&:hover {
border-color: var(--accent);
}
}
}
Source processing complicates this example because a build tool may transform nesting before the browser sees it. Feature queries should reflect the delivered CSS and the project's build pipeline, not only authoring syntax.
Font Technology and Format Tests
Modern supports conditions can test font technologies and formats with font-tech() and font-format() in contexts where the browser supports these conditions. They are useful when an advanced font capability changes the chosen source or styling strategy.
@supports font-tech(variations) {
.display-heading {
font-variation-settings: "wght" 720;
}
}
Always provide a readable font stack and ordinary weight fallback. Confirm browser policy because support for specialized condition functions may differ from basic declaration queries.
Grid Upgrade Pattern
A stacked baseline can become a responsive grid when Grid is available.
.cards {
display: block;
}
.card {
margin-block-end: 1rem;
}
@supports (display: grid) {
.cards {
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(min(100%, 16rem), 1fr)
);
gap: 1rem;
}
.card { margin-block-end: 0; }
}
The query coordinates the new layout and removal of fallback spacing, which is a strong reason to use @supports.
Subgrid Upgrade Pattern
Nested components can use independent tracks as a fallback and participate in parent tracks when subgrid is supported.
.card {
display: grid;
grid-template-rows: auto 1fr auto;
}
@supports (grid-template-rows: subgrid) {
.card {
grid-row: span 3;
grid-template-rows: subgrid;
}
}
Use subgrid only when shared alignment provides real value. The fallback can remain visually different while preserving readable content and actions.
Gap Fallback Pattern
Historically, layout gap support varied across contexts. A spacing fallback could use adjacent sibling margins and remove them when gap is available.
.actions > * + * {
margin-inline-start: 1rem;
}
@supports (gap: 1rem) {
.actions {
display: flex;
gap: 1rem;
}
.actions > * + * {
margin-inline-start: 0;
}
}
Current project targets may make this query unnecessary. Retain compatibility code only while it serves a supported browser.
Aspect-Ratio Upgrade Pattern
An older percentage-padding wrapper can provide a video ratio, while supporting browsers receive the clearer property.
.video {
position: relative;
padding-block-start: 56.25%;
}
@supports (aspect-ratio: 16 / 9) {
.video {
padding-block-start: 0;
aspect-ratio: 16 / 9;
}
}
The enhancement must also account for how child media is positioned in the fallback. Test the complete component rather than only the queried declaration.
Sticky Positioning Upgrade
Static positioning is a natural fallback for sticky side content.
.sidebar {
position: static;
}
@supports (position: sticky) {
.sidebar {
position: sticky;
inset-block-start: 1rem;
}
}
Support is not the only concern. Sticky behavior can fail because of containing blocks, overflow ancestors, missing space, or layout constraints, so browser recognition does not guarantee the desired result.
Backdrop Filter Upgrade
Translucent surfaces require a sufficiently opaque fallback so text remains readable without blur.
.panel {
background: rgb(255 255 255 / 0.96);
}
@supports (backdrop-filter: blur(12px)) {
.panel {
background: rgb(255 255 255 / 0.72);
backdrop-filter: blur(12px);
}
}
Measure performance on target devices and verify contrast over every possible background. Syntax support does not prove that a large animated blur is efficient or readable.
Fluid Function Upgrades
Functions such as clamp(), min(), and max() can replace stepped responsive values. A safe fixed value can precede the enhancement.
.title {
font-size: 2rem;
}
@supports (font-size: clamp(2rem, 5vw, 4rem)) {
.title {
font-size: clamp(2rem, 5vw, 4rem);
}
}
If project browser targets already guarantee these functions, the fallback and query add noise. Compatibility policy should drive the decision.
Dynamic Viewport Unit Upgrade
A classic viewport-height fallback can precede dynamic viewport units for mobile browser behavior.
.hero {
min-height: 100vh;
}
@supports (height: 100dvh) {
.hero {
min-height: 100dvh;
}
}
Even with dvh, avoid forcing text-heavy sections into a rigid viewport. Content flow and short-height testing remain important.
Modern Color Upgrade
A familiar color can serve as the baseline before an advanced color space or function is applied.
.button {
background: #315ed8;
}
@supports (color: oklch(60% 0.2 250)) {
.button {
background: oklch(60% 0.2 250);
}
}
Test contrast in every state. A browser's ability to parse a color does not establish that the chosen value meets accessibility requirements.
color-mix() Upgrade
Dynamic color mixing can replace a fixed hover color while keeping a known baseline.
.button:hover {
background: #2447aa;
}
@supports (color: color-mix(in srgb, red, blue)) {
.button:hover {
background: color-mix(
in srgb,
var(--brand),
black 20%
);
}
}
Use test values that exercise the syntax needed by the real declaration. Different color spaces can produce different visual and contrast outcomes.
Text Wrapping Enhancement
text-wrap: balance is a low-risk enhancement for short headings.
.hero-title {
text-align: center;
}
@supports (text-wrap: balance) {
.hero-title {
text-wrap: balance;
}
}
In many projects the query is unnecessary because an unsupported declaration is ignored and normal wrapping is already acceptable. Use a query only when other fallback declarations must change with it.
Container Query Upgrade
If container queries are optional under the support policy, gate the containment setup and component enhancement together.
.card {
display: grid;
gap: 1rem;
}
@supports (container-type: inline-size) {
.card-region {
container-type: inline-size;
}
@container (min-width: 30rem) {
.card {
grid-template-columns: 10rem minmax(0, 1fr);
}
}
}
Without support, the card stays in its usable stacked baseline. The query does not polyfill container behavior; it simply avoids applying the dependent rules.
Anchor Positioning Enhancement
CSS anchor positioning can attach a popover, tooltip, or supporting panel to another element without calculating coordinates in script. Because adopting it changes several related declarations, it is a useful feature-query candidate. The baseline can place the supporting content in normal flow where it remains readable and keyboard accessible.
.help-panel {
position: static;
margin-block-start: 0.75rem;
}
@supports (anchor-name: --help-trigger) {
.help-button {
anchor-name: --help-trigger;
}
.help-panel {
position: fixed;
position-anchor: --help-trigger;
inset-block-start: anchor(bottom);
inset-inline-start: anchor(left);
margin-block-start: 0.5rem;
}
}
The capability check does not provide popover semantics, dismissal, focus management, or collision handling by itself. Test zoom, scrolling, viewport edges, writing direction, and keyboard interaction. If the enhancement cannot preserve those behaviors, the normal-flow fallback may be the better product choice even in a supporting browser.
Feature-Gated Effects Still Need Accessible Fallbacks
Visual enhancements such as backdrop blur, advanced color mixing, masks, gradients, and filters can disappear or change under forced-color, increased-contrast, reduced-transparency, or print conditions. A successful support query only says that the syntax is recognized; it does not establish that the result remains perceivable under user preferences.
Build the baseline from solid surfaces, visible borders, semantic text, and non-color indicators. Add the effect inside @supports, then layer relevant preference queries after it so the user's environment can reduce or replace the enhancement.
.status {
border: 2px solid currentColor;
}
@supports (background: color-mix(in srgb, red, white)) {
.status {
background: color-mix(
in srgb,
currentColor 12%,
transparent
);
}
}
@media (forced-colors: active) {
.status {
background: Canvas;
}
}
Review the final cascade because the matching media rule and feature rule can both apply. Preference handling should remain authoritative where the design system requires it.
Combine @supports with @media
An enhancement may require both feature capability and sufficient viewport space.
@supports (display: grid) {
@media (min-width: 60rem) {
.layout {
display: grid;
grid-template-columns: 16rem minmax(0, 1fr);
}
}
}
The order can be reversed when that reads better. Keep conditional nesting shallow and organize it around the owning component.
Nested Feature Queries
One supported feature can contain a more advanced optional upgrade.
@supports (display: grid) {
.layout { display: grid; }
@supports (grid-template-columns: subgrid) {
.nested {
grid-template-columns: subgrid;
}
}
}
Deeply nested conditions become difficult to debug and can duplicate entire implementations. Prefer independent, small enhancements when possible.
Feature Detection Beats Browser Sniffing
User-agent logic asks which browser is running and then guesses its capabilities. That approach breaks when versions change, strings are altered, embedded browsers differ, or a browser implements the feature earlier than expected. @supports asks the capability question directly.
Browser-specific bugs may still require targeted investigation, but a product-wide compatibility architecture should begin with standards, support policy, and feature detection rather than names.
@supports vs @media
@supports asks whether the browser understands a CSS capability. @media asks whether the current output environment matches a condition such as width, print, pointer precision, or motion preference. A browser can support Grid while the viewport remains too narrow for a two-column layout, which is why both conditions may be needed.
Keep the distinction clear during debugging: one gate concerns implementation capability, while the other concerns the current environment.
@supports vs @container
A container query checks a qualifying ancestor's size or style. A supports query can determine whether the browser recognizes container setup, but it does not report whether a particular container condition currently matches.
Use @supports to guard an optional container-query architecture and @container to express the component's local responsive behavior.
@supports vs JavaScript Detection
Feature queries cover CSS syntax. They cannot ask whether the browser supports an HTML element, Web API, or JavaScript class such as IntersectionObserver, Geolocation, or a specific storage mechanism.
if ('IntersectionObserver' in window) {
// Use the supported JavaScript API.
}
Choose the native detection mechanism for the feature's domain. Do not add script classes to the document when CSS can handle the capability by itself.
CSS.supports() in JavaScript
JavaScript can query CSS declaration support through CSS.supports() when application behavior must coordinate with a CSS capability.
if (CSS.supports('display', 'grid')) {
console.log('Grid syntax is supported');
}
if (CSS.supports('selector(:has(*))')) {
console.log(':has() selector syntax is supported');
}
Keep visual adaptation in CSS whenever possible. Duplicating the same conditions across languages creates maintenance risk unless the behavior truly needs script awareness.
What @supports Cannot Guarantee
A true result does not prove pixel-identical output, complete freedom from browser bugs, accessible color choices, smooth performance, or correct interaction with every other feature. It also does not tell whether the feature is appropriate for the design.
Treat support as the first gate. Then validate actual rendering, accessibility, performance, and behavior in the project's browser matrix.
When Unsupported CSS Already Fails Safely
Browsers ignore declarations they do not understand while retaining earlier valid values. This often provides a fallback without @supports.
.title {
text-wrap: balance;
}
If normal wrapping is acceptable, no query is needed. Feature queries are most valuable when enabling the feature requires several coordinated declarations, removal of a fallback technique, or a structural change.
Do Not Query Universally Supported Features
A modern project whose supported browsers all implement Flexbox gains little from wrapping every flex container in @supports (display: flex). The extra condition obscures ownership and suggests compatibility complexity that no longer exists.
Periodically remove expired fallbacks and their tests. Support code has a maintenance cost and should remain only while the browser policy requires it.
Keep Fallbacks Simple
A fallback should preserve reading order, controls, and task completion. It does not need to emulate every alignment or decorative effect. A stacked list is often safer than a complicated float-based reproduction of a modern Grid dashboard.
Simplicity also reduces the declarations that the enhancement must neutralize. Shared typography, color, and semantics can remain outside both implementations.
Avoid Huge Parallel Implementations
Hundreds of old-layout rules followed by hundreds of modern replacements double the testing burden. Small product changes can update one branch while leaving the other inconsistent. If a fallback requires a complete second application, reconsider browser scope, server rendering, or the feature choice.
A strong architecture uses one shared baseline and focused upgrades. Retire legacy paths when usage, risk, and policy permit.
Organize Queries Near Their Owners
Keep a component's baseline and conditional enhancement together when the codebase style permits. A developer can then see which fallback declarations are removed or replaced. Global feature-query files tend to separate cause from effect and encourage selectors that reach across component boundaries.
For shared capability tokens or major application modes, document ownership and cascade layers. Conditional rules still participate in layer order, specificity, and source order after matching.
Debugging a Feature Query
First verify the condition independently with browser developer tools or CSS.supports(). Confirm that the tested declaration is valid and exactly matches the syntax used by the enhancement. Then inspect whether the enclosed rule is parsed and whether another declaration wins in the cascade.
If the query matches but the feature appears broken, investigate layout prerequisites, containing blocks, overflow, browser bugs, and feature interactions. Support recognition is not the same as a successful component implementation.
Testing Strategy
Test both paths. A modern development browser may always enter the enhancement, allowing the fallback to decay unnoticed. Use the oldest supported environment, a controlled build, or temporary condition inversion to inspect the baseline without permanently changing production behavior.
Verify content order, keyboard operation, focus visibility, zoom, localization, print, motion preferences, loading states, and dynamic updates in each relevant path. Add visual regression coverage where layout branches carry meaningful risk.
Browser Support Policy Comes First
Before adding a feature query, ask whether any supported browser lacks the feature and whether the unsupported result is still usable. Analytics, contractual environments, device fleets, and product risk should define that policy. A generic compatibility chart cannot decide which users the application supports.
If every target supports the feature, use it directly. If not, decide whether a simple fallback, conditional enhancement, polyfill, alternate interaction, or delayed adoption provides the best trade-off.
Feature Adoption Decision Workflow
Start by identifying the user benefit rather than the novelty of the property. Confirm support across the project's browser matrix and determine what unsupported CSS naturally does. If ignoring one declaration leaves a complete experience, use the declaration directly. If the feature requires coordinated changes, design the simplest acceptable baseline and place only the upgrade inside @supports.
Next, test whether the query accurately represents every dependency. A component using subgrid may also rely on Grid, while an anchor-positioned interface may depend on related positioning functions. Avoid using one loosely related declaration as a proxy for an entire browser version. Validate both branches with real content, keyboard input, zoom, preferences, and performance measurements.
Finally, establish an exit condition for the fallback. Record which supported browsers require it and review that evidence during platform updates. When the compatibility requirement ends, remove the query, fallback hacks, branch-specific tests, and documentation together. This prevents temporary progressive-enhancement code from becoming permanent architecture.
Feature Query Syntax Reference
| Syntax | Meaning |
|---|---|
@supports (display: grid) | Declaration syntax is supported |
@supports not (...) | Condition is not supported |
and | Every connected condition must match |
or | At least one connected condition must match |
selector(:has(*)) | Selector syntax is supported |
font-tech(variations) | Font technology is supported |
@media | Environment condition matches |
@container | Container condition matches |
CSS.supports() | JavaScript checks CSS support |
Common Mistakes and Better Approaches
| Mistake | Better Approach |
|---|---|
| No usable baseline | Write fallback CSS before the enhancement |
| Browser-name conditions | Detect the required feature |
| Testing only a property name | Test a complete property-value declaration |
| Assuming support means bug-free | Test actual browser behavior |
| Assuming support means fast | Measure performance on target devices |
| Querying every modern declaration | Rely on safe CSS error handling where sufficient |
| Deep conditional nesting | Keep enhancements shallow and independent |
| Two complete parallel stylesheets | Use a shared baseline with small upgrades |
| Testing HTML or JS APIs with CSS | Use domain-appropriate JavaScript detection |
| Never testing fallback paths | Include both paths in the compatibility matrix |
Common Interview Questions
What is @supports? It is a CSS conditional rule that makes enclosed styles eligible when a CSS support condition evaluates true.
What is a feature query? It is the common name for a CSS capability check written with @supports.
What is the basic syntax? Use a complete declaration inside parentheses: @supports (property: value) { ... }.
Can multiple features be tested? Yes. Use and, or, not, and parentheses to express the required logic.
Can selector support be tested? Yes. Use the selector() condition, for example @supports selector(:has(*)).
Is @supports the same as @media? No. Feature queries test CSS capability, while media queries test the current output environment or user preference.
Does @supports guarantee correct rendering? No. It confirms recognized support syntax, not freedom from bugs, visual differences, or performance problems.
Should every new CSS feature use @supports? No. Use it when compatibility requires a coordinated fallback or structural upgrade; otherwise normal CSS error handling may be enough.
Practical Review Checklist
- Start with semantic HTML and a usable CSS baseline.
- Test the exact declaration or selector required by the enhancement.
- Use positive progressive enhancement where it keeps logic clearer.
- Combine conditions only when every test represents a real dependency.
- Keep fallback layout simpler rather than visually identical.
- Use
selector()for selectors and declaration syntax for properties. - Use JavaScript detection for HTML elements and Web APIs.
- Do not mistake syntax support for quality or performance.
- Allow unsupported standalone declarations to fail safely when possible.
- Avoid querying features guaranteed by the browser support policy.
- Keep enhancements small and near the component that owns them.
- Limit nested conditional rules and duplicate implementations.
- Test baseline and enhanced paths with accessibility states.
- Review browser data and remove obsolete compatibility branches.
- Document why each remaining feature gate exists.
Key Takeaway
@supports asks whether the browser recognizes a CSS declaration, selector, or supported conditional capability. When the condition matches, its rules join the normal cascade. The most reliable pattern begins with a complete baseline and adds a focused enhancement that can safely disappear in browsers without the feature.
Use feature queries where adoption requires coordinated layout changes, fallback removal, or several dependent declarations. Do not add them around every modern property when unsupported CSS already fails acceptably or project targets guarantee support. Feature detection is more durable than browser sniffing, but it remains one part of compatibility engineering: define a browser policy, test both paths, measure performance, and preserve accessible functionality before and after the upgrade.