CSS Feature Queries
Container queries ask how wide a parent is. Media queries ask about the viewport. Feature queries ask something else entirely: does this browser understand this CSS? With @supports, you ship a reliable baseline, then unlock modern layout or selectors only where they work.
Progressive enhancement, not a gamble
A feature query is a capability gate. You do not delete the old styles — you keep a layout that works everywhere, then add a richer layer inside @supports when the engine can parse the feature you need (for example display: grid or gap on flex).
Basic @supports
The common form tests a property–value pair. If the browser can handle that declaration, the nested rules apply:
.card-list {
display: block;
}
.card-list .card {
margin-bottom: 1rem;
padding: 1rem;
border: 1px solid #e2e8f0;
border-radius: 0.75rem;
background: #fff;
}
@supports (display: grid) {
.card-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
gap: 1rem;
}
.card-list .card {
margin-bottom: 0;
}
}
Outside the block, cards stack with a simple margin. Inside @supports (display: grid), the same list becomes a responsive grid — older engines keep the stack; modern ones get the grid.
Combine conditions: and, or, not
You can require several features, accept alternatives, or invert a test:
@supports (display: grid) and (gap: 1rem) {
.toolbar {
display: grid;
grid-auto-flow: column;
gap: 0.75rem;
align-items: center;
}
}
@supports (display: flex) or (display: grid) {
.cluster {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
}
@supports not (display: grid) {
.card-list .card {
max-width: 36rem;
}
}
and tightens the gate (both must pass). or widens it. not styles the unsupported path — useful for a deliberate fallback, but keep that fallback honest and visible.
Test selectors with selector()
Some features are selectors, not properties. Wrap them in selector() so the query asks “can you parse this selector?”:
.nav a {
color: #0f172a;
text-decoration: none;
}
@supports selector(:has(*)) {
.nav:has(a:focus-visible) {
outline: 2px solid #2563eb;
outline-offset: 4px;
}
.card:has(img) .title {
font-weight: 700;
}
}
Without selector(), a bare :has() inside @supports is not the reliable pattern. With it, browsers that lack :has() simply skip the enhanced rules and keep the baseline link styles.
Who asks which question?
📊 Media, container, and feature queries
| Question you are asking | Prefer | Typical tool |
|---|---|---|
| How wide is the viewport / device context? | Page shell, breakpoints | @media (…) |
| How wide is this parent around a component? | Cards in unequal slots | @container (…) after container-type |
| Does the browser support this CSS? | Progressive enhancement | @supports (…) / selector(…) |
These tools stack cleanly: media for the page, container for the slot, @supports for capability. Do not use @supports to fake a breakpoint, or @media to fake a feature check.
Baseline first, enhance second
A practical pattern keeps shared look outside the gate, then upgrades layout inside:
.feature-panel {
padding: 1.25rem;
border-radius: 0.75rem;
background: #f8fafc;
color: #0f172a;
}
.feature-panel .lead {
margin: 0 0 0.75rem;
font-size: 1.125rem;
line-height: 1.5;
}
.feature-panel .actions {
display: block;
}
.feature-panel .actions a {
display: inline-block;
margin: 0.25rem 0.5rem 0.25rem 0;
padding: 0.5rem 0.875rem;
border-radius: 0.5rem;
background: #e2e8f0;
color: inherit;
text-decoration: none;
}
@supports (display: flex) and (gap: 0.5rem) {
.feature-panel .actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.feature-panel .actions a {
margin: 0;
}
}
Every browser gets padding, type, and tappable links. Flex + gap only appear when both are understood — no empty hole when either is missing.
Nesting and @supports together
Modern Toolkit pieces combine. Nesting organizes selectors; @supports still owns the capability gate:
.pricing {
padding: 1rem;
border: 1px solid #e2e8f0;
}
@supports (display: grid) {
.pricing {
display: grid;
gap: 1rem;
& .tier {
padding: 1rem;
background: #fff;
}
& .tier .price {
font-size: 1.5rem;
font-weight: 700;
}
}
}
If nesting itself is unsupported, keep a flat equivalent outside or accept that the nested form is part of the enhanced path — same progressive-enhancement mindset.
Try it yourself
- Style a list as a simple vertical stack, then wrap a Grid layout in
@supports (display: grid). - Require
gapwithandso flex/grid spacing does not half-apply. - Add
@supports not (display: grid)with a clear max-width fallback — confirm both paths look intentional. - Gate a
:has()rule with@supports selector(:has(*)). - Resist moving all visual styles inside
@supports; leave brand color and type in the baseline.
Common mistakes
- Putting the only usable layout inside
@supportswith no baseline — unsupported browsers get a broken page - Writing
@supports (display)without a value — feature queries need a property–value pair (orselector(…)) - Using
@supportswhere@mediabelongs (or the reverse) — capability ≠ viewport width - Testing a property the baseline already requires — gate only the enhancement
- Deep stacks of nested
@supportsfor tiny wins — prefer one clear enhanced layer
Summary
@supportsgates CSS on browser capability (feature queries)- Test with
(property: value), combine withand/or/not, and useselector()for selectors - Keep a solid baseline; enhance when support exists
- Remember the trio:
@media→ viewport,@container→ parent size,@supports→ capability - Modern Toolkit ends here — next module: Accessibility & Wrap-up (
accessibility)
🧠 Test Your Knowledge
Test Your Knowledge
Challenge yourself with this interactive quiz and see how well you understand the topic
📝 Instructions
- Read each question carefully
- Select the best answer for each question
- You can retake the quiz as many times as you want
- Your progress will be shown at the top