What is CSS @supports?
@supports (also called a CSS supports query or feature query) lets you apply CSS only
when the browser can understand a feature.
Think of it like:
“If you know how to do this… do it. Otherwise… don’t.”
This is perfect for progressive enhancement: you write a reliable baseline first, then “upgrade” the styling when modern features are supported.
-
Use
@supportswhen you want to adopt newer CSS without breaking older browsers. -
Use
@supports notwhen you want a fallback only for browsers that lack something. -
Use
@supports selector(...)when you need to detect support for newer selectors like:has().
.card {
border: 3px solid #111;
border-radius: 16px;
background: #fff;
padding: 16px;
}
@supports (backdrop-filter: blur(8px)) {
.card {
background: rgba(255, 255, 255, 0.65);
backdrop-filter: blur(8px);
}
}
.card {
border: 3px solid #111;
border-radius: 16px;
background: #fff;
padding: 16px;
}
@supports (background: oklch(70% 0.18 240)) {
.card {
background: oklch(96% 0.02 240);
border-color: oklch(55% 0.18 240);
}
}
.card {
border: 3px solid #111;
border-radius: 16px;
background: #fff;
padding: 16px;
}
@supports (text-wrap: balance) {
.card h3 {
text-wrap: balance;
}
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo-wrap {
min-height: 280px;
display: grid;
place-items: center;
padding: 24px;
background: #f4f4f4;
font-family: ui-monospace, SFMono-Regular, Menlo;
background-image: url("https://picsum.photos/900/410");
background-size: cover;
background-position: center;
}
.card {
width: min(520px, 92vw);
}
.card h3 {
margin: 0 0 8px;
font-size: 20px;
}
.card p {
margin: 0;
line-height: 1.4;
opacity: 0.9;
}
@supports in action
This card upgrades itself when the browser supports a feature. If not, it still looks totally fine.
Supports query syntax basics
A supports query looks like this:
-
@supports (property: value) { ... } -
@supports not (property: value) { ... } -
@supports (a) and (b) { ... } -
@supports (a) or (b) { ... }
The browser evaluates the condition. If it’s true, it applies the CSS inside. If it’s false, it ignores the whole block.
One key idea: @supports checks parsing support.
It’s basically asking: “Would this be valid CSS in this browser?”
.box {
background: #fff;
border: 3px solid #111;
padding: 16px;
}
@supports (display: grid) and (gap: 16px) {
.layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
}
.box {
background: #fff;
border: 3px solid #111;
padding: 16px;
}
@supports (display: grid) or (display: flex) {
.layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
}
.box {
background: #fff;
border: 3px solid #111;
padding: 16px;
}
@supports ((display: grid) and (gap: 16px)) or (display: flex) {
.layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
padding: 24px;
background: #f4f4f4;
font-family: ui-monospace, SFMono-Regular, Menlo;
}
.layout {
max-width: 720px;
margin: 0 auto;
}
.box h4 {
margin: 0 0 6px;
font-size: 16px;
}
.box p {
margin: 0;
line-height: 1.35;
opacity: 0.9;
}
Box A
Two columns when the feature query passes.
Box B
Otherwise, it stays stacked (still readable).
CSS supports query for properties and values
The most common pattern is checking a declaration:
(property: value).
If the browser recognizes that declaration as valid, the condition passes.
This is super useful for adopting modern layout and text features without risking broken CSS.
.stack {
display: flex;
flex-direction: column;
}
.stack > * + * {
margin-top: 12px;
}
@supports (gap: 12px) {
.stack {
gap: 12px;
}
.stack > * + * {
margin-top: 0;
}
}
.title {
font-size: 22px;
line-height: 1.15;
}
@supports (text-wrap: pretty) {
.title {
text-wrap: pretty;
}
}
.pill {
border-radius: 999px;
padding: 10px 14px;
background: #fff;
border: 3px solid #111;
}
@supports (color: oklch(60% 0.18 200)) {
.pill {
background: oklch(96% 0.03 200);
border-color: oklch(55% 0.18 200);
}
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
padding: 24px;
background: #f4f4f4;
font-family: ui-monospace, SFMono-Regular, Menlo;
display: grid;
place-items: center;
}
.panel {
width: min(410px, 92vw);
background: #fdfdfd;
border: 3px solid #111;
border-radius: 16px;
padding: 18px;
}
.card {
border: 2px dashed #bbb;
border-radius: 12px;
padding: 14px;
background: #fff;
}
.title {
margin: 0 0 10px;
}
.pill {
display: inline-block;
margin-top: 10px;
}
Feature queries keep your CSS modern without being reckless
OKLCH (if supported)Item 1Item 2Item 3
CSS @supports not
@supports not is the “inverse mode”.
It applies styles only when the browser does not support the tested feature.
This is great for fallbacks, but there’s a beginner trap:
you can often avoid @supports not by writing fallback CSS first, then upgrading with
@supports.
That approach is usually simpler.
Still, @supports not is handy when you want a very targeted fix.
.notice {
border: 3px solid #111;
border-radius: 14px;
padding: 14px;
background: rgba(255, 255, 255, 0.65);
backdrop-filter: blur(8px);
}
@supports not (backdrop-filter: blur(8px)) {
.notice {
background: #fff;
}
.notice strong {
text-decoration: underline;
}
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
padding: 24px;
background: #f4f4f4;
font-family: ui-monospace, SFMono-Regular, Menlo;
display: grid;
place-items: center;
}
.glass-zone {
width: min(440px, 92vw);
padding: 22px;
border-radius: 18px;
border: 3px solid #111;
background-image: url("https://picsum.photos/900/410");
background-size: cover;
background-position: center;
}
.notice {
max-width: 520px;
}
.notice p {
margin: 8px 0 0;
line-height: 1.35;
}
.notice strong {
font-weight: 700;
}
@supports notIf the browser can’t do backdrop blur, we switch to a simple high-contrast fallback.
Common @supports not gotchas
-
Parentheses matter.
@supports not (a) and (b)is not the same as@supports not ((a) and (b)). -
Test the thing you actually rely on.
If your layout needs
gap, test(gap: 1rem), not just(display: flex). - Prefer “fallback first”. It’s easier to read: baseline CSS, then upgrade block(s).
The progressive enhancement pattern you’ll use constantly
Here’s the pattern in one sentence:
Write a decent fallback first, then upgrade inside @supports.
In this example, we build a layout that works everywhere using simple block flow and margins. Then we upgrade it to a modern grid layout only when grid is supported.
.gallery {
display: block;
}
.tile + .tile {
margin-top: 12px;
}
@supports (display: grid) and (aspect-ratio: 1 / 1) {
.gallery {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.tile {
aspect-ratio: 9 / 12;
}
.tile + .tile {
margin-top: 0;
}
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
padding: 24px;
background: #f4f4f4;
font-family: ui-monospace, SFMono-Regular, Menlo;
}
.gallery {
max-width: 860px;
margin: 0 auto;
}
.tile {
border: 3px solid #111;
border-radius: 14px;
padding: 14px;
background: #fff;
display: grid;
gap: 10px;
}
.thumb {
height: 140px;
border-radius: 10px;
border: 2px dashed #bbb;
background-size: cover;
background-position: center;
}
.tile h4 {
margin: 0;
font-size: 16px;
}
.tile p {
margin: 0;
line-height: 1.35;
opacity: 0.9;
}
Tile 1
Fallback: stacked tiles.
Tile 2
Upgrade: CSS Grid + gap.
Tile 3
Optional: aspect-ratio if supported too.
Learn more about aspect-ratio in the CSS Aspect Ration Interactive Tutorial.
CSS @supports selector()
Some modern features are not “property/value” features. For example: new selectors.
That’s where @supports selector(...) comes in.
It checks whether the browser understands the selector inside.
Syntax:
-
@supports selector(.thing:has(.child)) { ... } -
@supports selector(:nth-child(1 of .class)) { ... } -
@supports not selector(:has(*)) { ... }
Example: @supports selector(:nth-child(1 of .class))
This one is very specific, and that’s the point: it detects support for the newer
“of selector list” syntax in :nth-child():
:nth-child(1 of .featured) means:
“The first child that matches .featured”
(not necessarily the first child overall).
If a browser doesn’t support that syntax, the selector is invalid.
So we can safely upgrade inside @supports selector(...).
.list .item {
border: 2px solid #bbb;
}
@supports selector(:nth-child(1 of .featured)) {
.list .item {
border-color: #bbb;
}
.list .item:nth-child(1 of .featured) {
border-color: #111;
transform: translateX(6px);
}
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
padding: 24px;
background: #f4f4f4;
font-family: ui-monospace, SFMono-Regular, Menlo;
display: grid;
place-items: center;
}
.panel {
width: min(460px, 92vw);
background: #fff;
border: 3px solid #111;
border-radius: 16px;
padding: 18px;
}
.panel h3 {
margin: 0 0 12px;
font-size: 18px;
}
.list {
display: grid;
gap: 10px;
}
.item {
border-radius: 12px;
padding: 12px 14px;
background: #fff;
transition: transform 200ms ease;
}
.item strong {
font-weight: 800;
}
Highlight the first .featured item (if supported)
Normal itemFeatured item (not first child)Normal itemFeatured item
Learn more about :nth-child(N of S) in the CSS Nth Child (N of Selector) Interactive Tutorial.
CSS @supports with :has()
:has() is the famous “parent selector-ish” feature (it’s more like a relational selector),
and it’s quite useful.
But it’s also a perfect candidate for feature queries, because older browsers might treat
:has() as invalid and drop the whole selector.
The safe pattern is:
-
Write a baseline style that does not depend on
:has(). -
Upgrade inside
@supports selector(:has(...)).
.card {
border: 3px solid #111;
border-radius: 16px;
padding: 16px;
background: #fff;
}
.card .tag {
display: inline-block;
border: 2px solid #111;
border-radius: 999px;
padding: 6px 10px;
}
@supports selector(.card:has(.tag)) {
.card:has(.tag) {
background: #f7f7f7;
}
.card:has(.tag.urgent) {
border-width: 5px;
transform: translateY(-2px);
}
}
.card {
border: 3px solid #111;
border-radius: 16px;
padding: 16px;
background: #fff;
}
@supports selector(.card:has(a:hover)) {
.card:has(a:hover) {
box-shadow: 0px 12px 0px 0px rgba(0, 0, 0, 0.15);
}
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
padding: 24px;
background: #f4f4f4;
font-family: ui-monospace, SFMono-Regular, Menlo;
display: grid;
place-items: center;
}
.grid {
width: min(460px, 92vw);
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.card h3 {
margin: 0 0 10px;
font-size: 18px;
}
.card p {
margin: 10px 0 0;
line-height: 1.35;
opacity: 0.9;
}
a {
color: inherit;
}
Normal card
This card has no tag.
Tagged card
infoThis one upgrades if
:has()is supported.Urgent card
urgentIt gets an extra “hey look at me” bump.
Hover-aware card
Hover this link to see a
:has(a:hover)upgrade (with snippet 2 active).
Learn more about :has() in the CSS :has() Pseudo-Class Interactive Tutorial.
“CSS supports hover” patterns
Let’s be precise: @supports does not check device capabilities like hover.
Hover capability is handled by @media, using media features like:
-
(hover: hover)for “this primary input can hover” -
(any-hover: hover)for “some input device can hover”
So when people say “supports hover”, they usually mean one of these:
-
“Only apply hover styles on hover-capable devices.” (
@media) -
“Only apply hover-related selector upgrades if supported.” (
@supports selector(...)) -
“Combine them for a safe, modern experience.” (
@supports+@media)
Combine @supports and @media (the practical
approach)
In this example:
- Baseline: the button looks fine everywhere.
- Upgrade 1: only add fancy effects if the browser supports them.
- Upgrade 2: only apply hover interactions on devices that actually hover.
.button {
border: 3px solid #111;
background: #fff;
border-radius: 999px;
padding: 12px 16px;
font-weight: 700;
cursor: pointer;
}
@supports (box-shadow: 0px 10px 0px 0px rgba(0, 0, 0, 0.2)) {
.button {
box-shadow: 0px 10px 0px 0px rgba(0, 0, 0, 0.2);
transition: transform 160ms ease, box-shadow 160ms ease;
}
@media (hover: hover) {
.button:hover {
transform: translateY(-2px);
box-shadow: 0px 12px 0px 0px rgba(0, 0, 0, 0.25);
}
}
}
.button {
border: 3px solid #111;
background: #fff;
border-radius: 999px;
padding: 12px 16px;
font-weight: 700;
cursor: pointer;
}
@supports selector(:focus-visible) {
.button:focus-visible {
outline: 4px solid #111;
outline-offset: 4px;
}
}
@media (hover: hover) {
.button:hover {
background: #f7f7f7;
}
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
padding: 34px 24px;
background: #f4f4f4;
font-family: ui-monospace, SFMono-Regular, Menlo;
display: grid;
place-items: center;
}
.row {
width: min(460px, 92vw);
display: grid;
gap: 14px;
justify-items: start;
}
.hint {
max-width: 680px;
line-height: 1.4;
opacity: 0.9;
}
On touch-only devices, hover styles won’t get in your way. Feature queries keep upgrades contained and safe.
Learn more about @media queries in the CSS Media Queries Interactive Tutorial.
CSS @supports not working: a debugging checklist
If your @supports block seems ignored, it’s usually one of these:
-
Your query is invalid.
The condition must be in parentheses, like
(display: grid), or useselector(...). -
You’re testing the wrong thing.
Test the feature you actually need, not a related feature.
For example, test
(gap: 1rem)if your layout needsgap. - You accidentally wrote CSS that’s valid but not supported the way you think. Some declarations parse but behave differently than expected. (Feature queries mostly check parsing, not “does this behave exactly like I imagine.”)
-
Your selector query should be
selector(...). If you’re trying to detect:has()or:nth-child(… of …), you want:@supports selector(...). -
Specificity/cascade is beating you.
Your
@supportsstyles might apply, but get overridden by later or more specific rules. - You put the “baseline” after the upgrade. If your fallback CSS appears later in the stylesheet, it can override your upgrade.
A quick mental model
@supports is a gate.
If the gate opens, the CSS inside joins the cascade like normal CSS.
If it doesn’t open, it’s as if that block doesn’t exist.
So when debugging, treat it like two separate stylesheets: the “baseline stylesheet” and the “upgrade stylesheet”. Then check validity, cascade order, and specificity.
Final tips (so future-you doesn’t grudge present-you)
-
Prefer fallback first, then upgrade with
@supports. -
Use
@supports selector(...)for selector features like:has()and:nth-child(… of …). -
Combine
@supportswith@media (hover: hover)when hover interactions matter. - Keep feature queries small and focused: one feature, one upgrade, easy to reason about.
CSS @supports Conclusion
CSS @supports is a powerful tool for writing modern, progressive-enhanced stylesheets.
By checking for feature support, you can safely adopt new CSS without breaking older browsers.
