@supports Feature Queries
New CSS features arrive all the time, but your visitors do not all update their browsers on the same day. A student in Kochi may be on the latest Chrome, while a shopkeeper in Rohtak still uses a two-year-old phone. You want to use modern CSS without breaking the page for anyone.
The answer is progressive enhancement: build a version that works everywhere, then add improvements for browsers that can handle them. In this lesson you will learn the simplest kind of fallback, which needs no special syntax, and then feature queries with @supports, which let you ask the browser "do you understand this?" before applying a group of rules.
Fallbacks with the cascade
Browsers skip any declaration they do not understand, and then carry on with the next one. So the easiest fallback is to write the old value first and the new value after it:
.hero {
height: 100vh; /* every browser understands this */
height: 100dvh; /* newer browsers: the real visible height on phones */
}
.badge {
background: #7c3aed;
background: linear-gradient(in oklch, #7c3aed, #db2777);
}An older browser reads 100vh, cannot parse 100dvh, and keeps 100vh. A newer browser applies both, and the later one wins. No special syntax is needed. Use this whenever the new feature is a single declaration that can simply replace the old one.
It is not enough when a new feature needs several properties to change together. Imagine a frosted-glass header: with backdrop-filter you want a see-through background, but without it the background must stay solid, or the text becomes unreadable. You need to change the background only if the blur works. That is what @supports is for.
The @supports rule
A feature query wraps rules in a condition. The rules inside apply only if the browser understands the declaration in the brackets:
@supports (backdrop-filter: blur(8px)) {
.site-header {
background: rgb(255 255 255 / 60%);
backdrop-filter: blur(8px);
}
}Write a complete property: value pair inside round brackets. Writing only the property name, such as @supports (backdrop-filter), is invalid. You can combine conditions with three keywords:
| Condition | Applies when |
|---|---|
@supports (a: b) | The declaration is supported |
@supports not (a: b) | The declaration is not supported |
@supports (a: b) and (c: d) | Both are supported |
@supports (a: b) or (c: d) | At least one is supported |
Every condition needs its own brackets. You cannot mix and and or at the same level; group them with extra brackets, for example ((a: b) or (c: d)) and (e: f). The or form is handy for prefixed properties: @supports (mask-image: none) or (-webkit-mask-image: none).
Here is the frosted header, built the progressive way. The plain version comes first, outside any query, and the enhancement goes inside:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Frosted header with @supports</title>
<style>
body { margin: 0; font-family: system-ui, sans-serif; }
.site-header {
position: sticky;
top: 0;
padding: 0.9rem 1rem;
background: #ffffff; /* safe for every browser */
border-bottom: 1px solid #e2e8f0;
font-weight: 700;
}
@supports (backdrop-filter: blur(10px)) {
.site-header {
background: rgb(255 255 255 / 55%); /* see-through only when blur works */
backdrop-filter: blur(10px);
}
}
.hero { height: 16rem; background: url("https://course.ethicscomputer.in/assets/samples/landscape.svg") center / cover, #1e3a8a; }
main { padding: 1rem; line-height: 1.6; }
</style>
</head>
<body>
<header class="site-header">Pune Weekend Treks</header>
<div class="hero" role="img" aria-label="Hills and a river at sunrise"></div>
<main>
<p>Scroll the result so the photo passes under the header. In a browser with backdrop-filter, the header is see-through and blurred. In a browser without it, the header stays solid white, so the text is always readable.</p>
<p>Treks leave every Saturday at 5 am from Shivajinagar. The fee of ₹1,499 includes breakfast and a guide.</p>
<p>Bring water, a cap and good shoes.</p>
</main>
</body>
</html>Scroll inside the result: the photo blurs behind the header. To see the fallback, change the query to a made-up property such as (backdrop-blurring: 10px) and run again. The header becomes solid white, which is exactly what an older browser would show.
Testing selectors with selector()
@supports selector(...) checks whether the browser understands a selector, not a property. This is how you protect rules that use newer pseudo-classes such as :has():
/* Basic: a red border on the invalid field itself */
.field input:user-invalid {
border-color: #dc2626;
}
/* Enhanced: also tint the whole field group, using :has() */
@supports selector(:has(a)) {
.field:has(input:user-invalid) {
background: #fef2f2;
}
}:has() now works in all current browsers (see :has(), :is() and :where()), but a test like this still protects visitors on older devices. Two more functions follow the same pattern: font-tech() and font-format() check whether the browser can use a type of font, for example @supports font-tech(color-COLRv1) for colour fonts.
Checking from JavaScript
The same question can be asked from JavaScript with CSS.supports(). It returns true or false. You can pass a property and a value, as in CSS.supports("display", "grid"), or a complete condition string, as in this example:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>What does my browser support?</title>
<style>
body { font-family: system-ui, sans-serif; padding: 1rem; }
li { padding: 0.3rem 0.6rem; margin-block: 0.3rem; border-radius: 6px; list-style: none; }
.yes { background: #dcfce7; }
.no { background: #fee2e2; }
</style>
</head>
<body>
<h1>Feature check</h1>
<ul id="results"></ul>
<script>
const checks = [
"(display: grid)",
"(color: oklch(60% 0.2 30))",
"selector(:has(a))",
"(height: 100dvh)",
"(text-wrap: pretty)",
"(anchor-name: --menu)",
"(animation-timeline: scroll())",
];
const list = document.querySelector("#results");
for (const condition of checks) {
const ok = CSS.supports(condition);
const item = document.createElement("li");
item.className = ok ? "yes" : "no";
item.textContent = (ok ? "Supported: " : "Not supported: ") + condition;
list.append(item);
}
</script>
</body>
</html>Open this page in two different browsers, or on your phone, and compare the lists. Results depend on the browser and its version, which is exactly why feature queries exist.
Write the fallback first, enhance second
There are two ways to organise feature queries. The recommended one puts the basic styles outside and the improvement inside a positive query:
/* 1. Works everywhere */
.gallery { display: flex; flex-wrap: wrap; gap: 1rem; }
.gallery > * { flex: 1 1 12rem; }
/* 2. Better where supported */
@supports (grid-template-rows: subgrid) {
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
}
.gallery > * {
display: grid;
grid-row: span 3;
grid-template-rows: subgrid; /* line up titles, text and buttons across cards */
}
}You can also write @supports not (...) for styles that only the old browsers should get. That works in every current browser, but a very old browser that does not know @supports at all skips the whole block, including the not rules. Keeping fallbacks outside any query avoids that trap. With CSS Nesting, you can also put an @supports block inside a rule, next to the styles it improves.
What @supports cannot tell you
A feature query only checks whether the browser can parse the declaration. It does not prove the feature works fully or without bugs. The classic example is gap: browsers understood gap for Grid years before they supported it in Flexbox, so @supports (gap: 1rem) said "yes" in browsers where flex gaps did nothing.
Also, do not wrap features that already work everywhere. Flexbox, Grid, custom properties and oklch() do not need a feature query today. Check a feature's support on MDN, where many pages show a Baseline label when a feature works across the major browsers, and save @supports for genuinely new features.
Common mistakes
- A property without a value.
@supports (backdrop-filter)is invalid; write(backdrop-filter: blur(1px)). - Missing brackets.
@supports display: gridis invalid; write@supports (display: grid). - Mixing
andandorwithout grouping. Add brackets to show which conditions belong together. - Putting the fallback inside
not. Put basic styles outside any query and enhance inside a positive one. - Trusting a query too much.
@supportstests parsing, not perfect support; test in real browsers. - Querying everything. Widely supported features need no query.
Key points
- Progressive enhancement means a working basic version for everyone, plus improvements where supported.
- For a single declaration, write the old value first and the new one after; unknown values are skipped.
@supports (property: value) { ... }applies a group of rules only where the declaration is understood.- Combine conditions with
not,andandor, with brackets around every condition. selector()tests selectors such as:has();CSS.supports()asks the same question from JavaScript.- Feature queries check parsing, not perfect behaviour, and are only needed for newer features.
Exercise
A results page for a Jaipur coaching centre shows student cards. Write CSS so that:
- every browser gets a simple one-column list of cards with a solid white card background;
- browsers that support
:has()give a card a gold border when its checkbox "Shortlist" is checked; - browsers that support
backdrop-filterget a see-through blurred sticky page header, while others keep a solid header.
Show solution
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Results</title>
<style>
body { margin: 0; font-family: system-ui, sans-serif; background: #e0e7ff; }
header { position: sticky; top: 0; padding: 1rem; background: #312e81; color: white; }
@supports (backdrop-filter: blur(8px)) {
header { background: rgb(49 46 129 / 70%); backdrop-filter: blur(8px); }
}
.cards { display: grid; gap: 1rem; padding: 1rem; max-width: 30rem; }
.card { background: white; padding: 1rem; border: 3px solid transparent; border-radius: 10px; }
@supports selector(:has(a)) {
.card:has(input:checked) { border-color: #ca8a04; }
}
</style>
</head>
<body>
<header>Jaipur Coaching Centre: Mock Test Results</header>
<div class="cards">
<article class="card"><h2>Aarav</h2><p>Score: 182 / 200</p><label><input type="checkbox"> Shortlist</label></article>
<article class="card"><h2>Ananya</h2><p>Score: 176 / 200</p><label><input type="checkbox"> Shortlist</label></article>
</div>
</body>
</html>The solid header and plain cards work everywhere. Each enhancement sits in its own positive @supports block, so a browser only gets the see-through header if it can blur, and only gets the gold border if it understands :has().
Quick Quiz
1. Which feature query is written correctly?
Each condition is a complete property: value pair inside round brackets.
2. What happens when a browser meets height: 100dvh; and does not understand dvh?
Unknown declarations are dropped one by one, which makes "old value first, new value second" a simple fallback.
3. How do you test whether a browser understands the :has() selector?
The selector() function checks selectors instead of property declarations.
4. Why can @supports (gap: 1rem) give a misleading answer?
Feature queries test parsing, not whether every use of the feature behaves correctly.
Learn CSS with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
