CSS3 & Responsive Design

Feature Queries with @supports

20 min Lesson 56 of 60

What Are Feature Queries?

Feature queries are a CSS mechanism that lets you test whether the browser supports a specific CSS property-value pair before applying a block of styles. They use the @supports at-rule, which works similarly to @media but instead of testing viewport characteristics, it tests CSS feature support. If the browser supports the feature, the styles inside the @supports block are applied. If not, they are silently ignored.

Feature queries solve a fundamental problem in web development: how do you use new CSS features while ensuring older browsers still get a usable experience? Before @supports, developers relied on JavaScript-based feature detection libraries like Modernizr, or they used hacks and workarounds to detect browser capabilities. With @supports, feature detection is built directly into CSS -- it is fast, reliable, and does not require any JavaScript.

The concept behind feature queries is called progressive enhancement: you start with a baseline experience that works everywhere, then layer on enhanced styles for browsers that support advanced features. This approach ensures no user is left with a broken layout, while users with modern browsers get the best possible experience. In this lesson, you will master every aspect of @supports, from basic syntax to advanced patterns for building robust, future-proof stylesheets.

Basic @supports Syntax

The simplest form of @supports tests whether the browser understands a specific CSS declaration (a property-value pair). If the browser recognizes the property and considers the value valid, the condition evaluates to true and the styles inside the block are applied.

Basic @supports Syntax

/* Test if the browser supports display: grid */
@supports (display: grid) {
    .container {
        display: grid;
        grid-template-columns: repeat(3, 1fr);
        gap: 2rem;
    }
}

/* Test if the browser supports backdrop-filter */
@supports (backdrop-filter: blur(10px)) {
    .glass-panel {
        background: rgba(255, 255, 255, 0.1);
        backdrop-filter: blur(10px);
        -webkit-backdrop-filter: blur(10px);
    }
}

/* Test if the browser supports aspect-ratio */
@supports (aspect-ratio: 16 / 9) {
    .video-wrapper {
        aspect-ratio: 16 / 9;
        width: 100%;
    }
}

/* Test if the browser supports the clamp() function */
@supports (font-size: clamp(1rem, 2vw, 2rem)) {
    .responsive-text {
        font-size: clamp(1rem, 1.5vw + 0.5rem, 2rem);
    }
}
Note: The condition inside @supports must be a complete CSS declaration wrapped in parentheses -- both the property and the value. You cannot test for a property alone without a value (e.g., @supports (display) is not valid). The browser checks if it can parse and would accept that specific declaration. It does not actually apply the test declaration to any element; it only checks whether it understands it.

The Importance of the Value

The value you test matters. A browser might support a property but not a specific value of that property. For example, all browsers support display, but not all support display: grid or display: subgrid. Always test the exact property-value combination you intend to use.

Testing Specific Values

/* This tests if grid is supported */
@supports (display: grid) {
    /* Modern grid layout */
}

/* This tests if subgrid is supported (newer feature) */
@supports (grid-template-columns: subgrid) {
    .nested-grid {
        display: grid;
        grid-template-columns: subgrid;
    }
}

/* Test a specific color function */
@supports (color: oklch(0.7 0.15 180)) {
    :root {
        --primary: oklch(0.6 0.2 250);
        --secondary: oklch(0.7 0.15 180);
    }
}

/* Test container query support */
@supports (container-type: inline-size) {
    .card-wrapper {
        container-type: inline-size;
    }
}

/* Test scroll-driven animations */
@supports (animation-timeline: scroll()) {
    .progress-bar {
        animation-timeline: scroll();
    }
}

Logical Operators: not, and, or

Feature queries support three logical operators that let you create complex conditions. These operators work similarly to their counterparts in @media queries and follow the same precedence rules.

The not Operator

The not operator negates a condition. It is useful for providing fallback styles when a feature is not supported. The not keyword must be followed by the condition in parentheses.

Using the not Operator

/* Apply styles when grid is NOT supported */
@supports not (display: grid) {
    .container {
        display: flex;
        flex-wrap: wrap;
    }

    .container > * {
        flex: 0 0 calc(33.333% - 2rem);
        margin: 1rem;
    }
}

/* Fallback when backdrop-filter is not available */
@supports not (backdrop-filter: blur(10px)) {
    .glass-panel {
        background: rgba(255, 255, 255, 0.85);
        /* Solid semi-transparent background as fallback */
    }
}

/* Fallback when :has() is not supported */
@supports not (selector(:has(*))) {
    /* JavaScript-based alternatives or simpler styling */
    .parent-with-child {
        border: 2px solid blue;
    }
}

The and Operator

The and operator combines multiple conditions that must all be true. This is useful when your enhanced styles depend on multiple features being available simultaneously.

Using the and Operator

/* Both grid and gap must be supported */
@supports (display: grid) and (gap: 1rem) {
    .gallery {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
        gap: 1.5rem;
    }
}

/* Require both custom properties and clamp() */
@supports (--custom: value) and (font-size: clamp(1rem, 2vw, 3rem)) {
    :root {
        --heading-size: clamp(1.5rem, 3vw + 0.5rem, 3rem);
        --body-size: clamp(1rem, 1.2vw + 0.5rem, 1.25rem);
    }

    h1 { font-size: var(--heading-size); }
    body { font-size: var(--body-size); }
}

/* Multiple conditions for a complex enhancement */
@supports (display: grid) and (aspect-ratio: 1) and (object-fit: cover) {
    .image-grid {
        display: grid;
        grid-template-columns: repeat(3, 1fr);
        gap: 0.5rem;
    }

    .image-grid img {
        aspect-ratio: 1;
        object-fit: cover;
        width: 100%;
    }
}

The or Operator

The or operator is true when at least one condition is met. This is particularly useful when a feature has vendor-prefixed versions and you want to check if any of them is supported.

Using the or Operator

/* Check for any supported version of backdrop-filter */
@supports (backdrop-filter: blur(10px)) or (-webkit-backdrop-filter: blur(10px)) {
    .frosted-glass {
        background: rgba(255, 255, 255, 0.15);
        backdrop-filter: blur(12px);
        -webkit-backdrop-filter: blur(12px);
    }
}

/* Check for any sticky positioning support */
@supports (position: sticky) or (position: -webkit-sticky) {
    .sticky-header {
        position: -webkit-sticky;
        position: sticky;
        top: 0;
        z-index: 100;
    }
}

/* Check for different modern layout features */
@supports (display: grid) or (display: flex) {
    .layout {
        /* At minimum flex or grid is available */
    }
}

Combining Operators

You can combine not, and, and or operators, but you must use parentheses to make the grouping explicit. CSS does not allow mixing and and or without parentheses to avoid ambiguity.

Combining Multiple Operators

/* Explicit grouping with parentheses */
@supports ((display: grid) and (gap: 1rem)) or (display: flex) {
    /* Grid with gap, OR at least flexbox */
    .modern-layout {
        /* styles here */
    }
}

/* not combined with and */
@supports (display: grid) and (not (grid-template-columns: subgrid)) {
    /* Grid is supported, but subgrid is NOT */
    .fallback-grid {
        display: grid;
        grid-template-columns: repeat(3, 1fr);
        /* Manual column sizing instead of subgrid */
    }
}

/* Complex multi-condition query */
@supports ((backdrop-filter: blur(10px)) or (-webkit-backdrop-filter: blur(10px)))
      and (not (hanging-punctuation: first)) {
    /* Has blur support but not hanging-punctuation */
}
Warning: You cannot mix and and or at the same level without explicit grouping parentheses. The query @supports (a: b) and (c: d) or (e: f) is invalid. You must write either @supports ((a: b) and (c: d)) or (e: f) or @supports (a: b) and ((c: d) or (e: f)). The browser will ignore the entire @supports block if the condition syntax is malformed.

Testing Selector Support with @supports selector()

Beyond testing property-value pairs, @supports can also test whether the browser understands a specific CSS selector. This uses the selector() function inside the @supports condition. This is especially valuable for newer pseudo-classes like :has(), :is(), and :where().

Testing Selector Support

/* Test if the browser supports :has() */
@supports selector(:has(*)) {
    /* Use :has() for parent selection */
    .form-group:has(:invalid) {
        border-left: 3px solid #e74c3c;
        padding-left: 1rem;
    }

    .card:has(img) {
        grid-template-rows: auto 1fr auto;
    }

    .nav:has(.dropdown:hover) {
        background: rgba(0, 0, 0, 0.05);
    }
}

/* Test for :focus-visible support */
@supports selector(:focus-visible) {
    button:focus {
        outline: none;
    }

    button:focus-visible {
        outline: 2px solid #3498db;
        outline-offset: 2px;
    }
}

/* Test for :is() support */
@supports selector(:is(a, b)) {
    :is(h1, h2, h3, h4) {
        line-height: 1.2;
        margin-bottom: 0.5em;
    }

    :is(article, section, aside) :is(h1, h2, h3) {
        color: #333;
    }
}

/* Test for :where() support */
@supports selector(:where(a)) {
    :where(ul, ol) {
        padding-left: 1.5rem;
    }
}

/* Test for :user-valid / :user-invalid */
@supports selector(:user-valid) {
    input:user-valid {
        border-color: #2ecc71;
    }

    input:user-invalid {
        border-color: #e74c3c;
    }
}
Note: The selector() function tests whether the browser can parse the selector, not whether any elements currently match it. Even if :has(*) would match zero elements on the page, the @supports selector(:has(*)) condition still evaluates to true as long as the browser understands the :has() pseudo-class.

Progressive Enhancement with @supports

Progressive enhancement is the core philosophy behind feature queries. You write a baseline that works in all browsers, then enhance the experience for browsers that support advanced features. This ensures no one gets a broken experience while modern browsers get the best version.

Progressive Enhancement Pattern

/* STEP 1: Baseline -- works in every browser */
.card-grid {
    display: block;
}

.card-grid .card {
    max-width: 400px;
    margin: 0 auto 2rem;
}

/* STEP 2: Enhancement for flexbox (very wide support) */
@supports (display: flex) {
    .card-grid {
        display: flex;
        flex-wrap: wrap;
        gap: 1.5rem;
    }

    .card-grid .card {
        flex: 1 1 300px;
        max-width: none;
        margin: 0;
    }
}

/* STEP 3: Further enhancement for grid (wide support) */
@supports (display: grid) {
    .card-grid {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
        gap: 1.5rem;
    }

    .card-grid .card {
        flex: unset;
    }
}

/* STEP 4: Even further enhancement for subgrid */
@supports (grid-template-columns: subgrid) {
    .card-grid .card {
        display: grid;
        grid-template-rows: subgrid;
        grid-row: span 3;
    }
}

In this example, each @supports block adds a layer of enhancement. The most basic browsers see stacked cards. Flexbox-capable browsers see a flexible multi-column layout. Grid-capable browsers get an even better auto-fit layout. And subgrid browsers get perfectly aligned card content across the grid. Every user gets a functional layout; modern users get the best one.

Fallback Layouts: Flexbox Fallback for Grid

One of the most common use cases for @supports is providing a flexbox-based fallback for CSS Grid layouts. While Grid support is now nearly universal, this pattern demonstrates the technique well and is still relevant for features like subgrid.

Flexbox Fallback for Grid

/* Fallback layout with flexbox */
.dashboard {
    display: flex;
    flex-wrap: wrap;
    margin: -0.75rem;
}

.dashboard .widget {
    flex: 1 1 300px;
    margin: 0.75rem;
}

.dashboard .widget--wide {
    flex: 1 1 100%;
}

/* Enhanced layout with grid */
@supports (display: grid) {
    .dashboard {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
        gap: 1.5rem;
        margin: 0;
    }

    .dashboard .widget {
        margin: 0;
    }

    .dashboard .widget--wide {
        grid-column: 1 / -1;
    }

    .dashboard .widget--tall {
        grid-row: span 2;
    }
}

/* Another common pattern: sidebar layout */
.page-layout {
    display: flex;
    flex-wrap: wrap;
}

.page-layout .main {
    flex: 1 1 600px;
    min-width: 0;
}

.page-layout .sidebar {
    flex: 0 0 300px;
}

@supports (display: grid) {
    .page-layout {
        display: grid;
        grid-template-columns: 1fr 300px;
        gap: 2rem;
    }

    @media (max-width: 900px) {
        .page-layout {
            grid-template-columns: 1fr;
        }
    }
}

Feature Detection vs Browser Detection

Feature detection and browser detection are two fundamentally different approaches to handling cross-browser compatibility. Understanding why feature detection is superior is essential for writing maintainable CSS.

Browser detection (also called "user agent sniffing") involves checking which browser the user is running and applying specific styles or code paths based on the browser name and version. This approach is fragile, hard to maintain, and prone to errors.

Feature detection involves checking whether a specific capability is available, regardless of which browser provides it. This is what @supports does. It is more robust because it asks "can you do this?" instead of "who are you?"

Feature Detection vs Browser Detection

/* BAD: Browser detection with CSS hacks */
/* These target specific browsers and break easily */

/* Targeting only Safari (fragile hack) */
@media not all and (min-resolution: 0.001dpcm) {
    /* Safari-only styles -- DO NOT DO THIS */
}

/* Targeting only Firefox (fragile hack) */
@-moz-document url-prefix() {
    /* Firefox-only styles -- DO NOT DO THIS */
}

/* GOOD: Feature detection with @supports */
/* Test the actual capability, not the browser */

@supports (backdrop-filter: blur(10px)) {
    /* Any browser that supports this gets the style */
    .overlay {
        backdrop-filter: blur(10px);
        background: rgba(0, 0, 0, 0.3);
    }
}

@supports not (backdrop-filter: blur(10px)) {
    /* Any browser that does NOT support this gets the fallback */
    .overlay {
        background: rgba(0, 0, 0, 0.7);
    }
}

/* Why feature detection is better:
   1. New browsers automatically get correct styles
   2. If a browser adds support, it works immediately
   3. You do not need to update a browser list
   4. It tests actual capability, not identity */
Tip: Browser detection has one legitimate use case: working around confirmed, specific browser bugs where the feature technically exists but is broken. Even then, it is better to use a targeted workaround that will not affect other browsers. In virtually all other cases, feature detection with @supports is the correct approach.

Common @supports Patterns

Let us explore the most common real-world patterns for using @supports. These are patterns you will reach for repeatedly in production code.

Testing Grid Support

Grid Feature Queries

/* Basic grid support */
@supports (display: grid) {
    .photo-grid {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
        gap: 1rem;
    }
}

/* Subgrid support */
@supports (grid-template-columns: subgrid) {
    .card-grid .card {
        display: grid;
        grid-row: span 3;
        grid-template-rows: subgrid;
    }
}

/* Masonry layout (experimental) */
@supports (grid-template-rows: masonry) {
    .masonry-grid {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
        grid-template-rows: masonry;
        gap: 1rem;
    }
}

Testing clamp() and Modern Functions

Modern CSS Functions

/* clamp() for fluid typography */
@supports (font-size: clamp(1rem, 2vw, 3rem)) {
    h1 {
        font-size: clamp(2rem, 4vw + 0.5rem, 4rem);
    }

    p {
        font-size: clamp(1rem, 1.2vw + 0.5rem, 1.25rem);
    }
}

/* Fallback for browsers without clamp() */
@supports not (font-size: clamp(1rem, 2vw, 3rem)) {
    h1 {
        font-size: 2rem;
    }

    @media (min-width: 768px) {
        h1 {
            font-size: 3rem;
        }
    }

    @media (min-width: 1200px) {
        h1 {
            font-size: 4rem;
        }
    }
}

/* min() and max() functions */
@supports (width: min(90%, 1200px)) {
    .container {
        width: min(90%, 1200px);
        margin-inline: auto;
    }
}

Testing backdrop-filter

Frosted Glass Effect with Fallback

/* Base styles (fallback) */
.modal-overlay {
    background: rgba(0, 0, 0, 0.7);
}

.glass-card {
    background: rgba(255, 255, 255, 0.9);
    border: 1px solid rgba(255, 255, 255, 0.3);
}

/* Enhanced with backdrop-filter */
@supports (backdrop-filter: blur(10px)) or (-webkit-backdrop-filter: blur(10px)) {
    .modal-overlay {
        background: rgba(0, 0, 0, 0.3);
        backdrop-filter: blur(8px);
        -webkit-backdrop-filter: blur(8px);
    }

    .glass-card {
        background: rgba(255, 255, 255, 0.15);
        backdrop-filter: blur(20px) saturate(180%);
        -webkit-backdrop-filter: blur(20px) saturate(180%);
        border: 1px solid rgba(255, 255, 255, 0.2);
    }
}

Testing :has() Support

Parent Selection with :has() Fallback

/* Fallback: rely on classes added by JavaScript */
.form-group.has-error {
    border-left: 3px solid #e74c3c;
}

.card.has-image {
    display: grid;
    grid-template-columns: 200px 1fr;
}

/* Enhanced: use :has() for automatic detection */
@supports selector(:has(*)) {
    .form-group:has(:invalid:not(:placeholder-shown)) {
        border-left: 3px solid #e74c3c;
    }

    .form-group:has(:focus) {
        box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
    }

    .card:has(img) {
        display: grid;
        grid-template-columns: 200px 1fr;
    }

    .card:has(img):has(.badge) {
        position: relative;
    }
}

/* Another :has() pattern: quantity queries */
@supports selector(:has(*)) {
    /* Style differently when a list has more than 5 items */
    .tag-list:has(:nth-child(6)) {
        flex-wrap: wrap;
        gap: 0.5rem;
    }

    /* Style parent based on empty state */
    .search-results:has(:empty) {
        display: none;
    }
}

@supports with Custom Properties

You can test whether the browser supports CSS custom properties (CSS variables) using @supports. Since custom properties are now universally supported in modern browsers, this is mainly useful as a safety net for legacy browser support or to test for specific custom property features.

Testing Custom Property Support

/* Test if custom properties are supported */
@supports (--custom: value) {
    :root {
        --primary: #3498db;
        --secondary: #2ecc71;
        --text: #333;
        --bg: #ffffff;
        --spacing: 1.5rem;
        --radius: 8px;
    }

    .button {
        background: var(--primary);
        color: white;
        padding: var(--spacing);
        border-radius: var(--radius);
    }
}

/* Fallback for browsers without custom properties */
@supports not (--custom: value) {
    .button {
        background: #3498db;
        color: white;
        padding: 1.5rem;
        border-radius: 8px;
    }
}

/* Test custom properties combined with other features */
@supports (--a: b) and (color: oklch(0.5 0.2 240)) {
    :root {
        --primary: oklch(0.55 0.2 250);
        --primary-light: oklch(0.75 0.15 250);
        --primary-dark: oklch(0.40 0.2 250);
    }
}

CSS.supports() in JavaScript

The @supports functionality is also available in JavaScript through the CSS.supports() method. This allows you to run the same feature detection in your scripts, which is useful for adding classes, loading polyfills, or choosing between different JavaScript implementations based on CSS capabilities.

Using CSS.supports() in JavaScript

// Method 1: Two arguments (property, value)
const hasGrid = CSS.supports('display', 'grid');
const hasSubgrid = CSS.supports('grid-template-columns', 'subgrid');
const hasBackdropFilter = CSS.supports('backdrop-filter', 'blur(10px)');
const hasContainerQueries = CSS.supports('container-type', 'inline-size');

console.log('Grid:', hasGrid);           // true in modern browsers
console.log('Subgrid:', hasSubgrid);     // true in newer browsers
console.log('Backdrop:', hasBackdropFilter); // true in most browsers

// Method 2: Single argument (full condition string)
const hasGridAndGap = CSS.supports('(display: grid) and (gap: 1rem)');
const hasAnyBlur = CSS.supports(
    '(backdrop-filter: blur(10px)) or (-webkit-backdrop-filter: blur(10px))'
);

// Method 3: Testing selector support
const hasHas = CSS.supports('selector(:has(*))');
const hasFocusVisible = CSS.supports('selector(:focus-visible)');

// Practical usage: add feature classes to the body
if (CSS.supports('display', 'grid')) {
    document.body.classList.add('supports-grid');
}

if (CSS.supports('selector(:has(*))')) {
    document.body.classList.add('supports-has');
} else {
    // Load a polyfill or use JavaScript-based parent selection
    loadScript('has-polyfill.js');
}

// Practical usage: choose between implementations
function createLayout() {
    if (CSS.supports('display', 'grid')) {
        return createGridLayout();
    } else {
        return createFlexLayout();
    }
}

// Feature detection before applying dynamic styles
function applyGlassEffect(element) {
    if (CSS.supports('backdrop-filter', 'blur(10px)')
        || CSS.supports('-webkit-backdrop-filter', 'blur(10px)')) {
        element.style.backdropFilter = 'blur(10px)';
        element.style.background = 'rgba(255, 255, 255, 0.1)';
    } else {
        element.style.background = 'rgba(255, 255, 255, 0.85)';
    }
}
Tip: CSS.supports() in JavaScript is synchronous and very fast. It does not cause layout recalculation or reflow. You can safely call it multiple times during page initialization to set up feature flags. Consider creating a utility object that caches the results for reuse throughout your application.

Combining @supports with @media

Feature queries and media queries can be used together to create highly targeted styles that depend on both browser capabilities and viewport characteristics. You can nest them inside each other in either order.

Combining @supports and @media

/* @media inside @supports */
@supports (display: grid) {
    .product-grid {
        display: grid;
        grid-template-columns: 1fr;
        gap: 1rem;
    }

    @media (min-width: 600px) {
        .product-grid {
            grid-template-columns: repeat(2, 1fr);
        }
    }

    @media (min-width: 900px) {
        .product-grid {
            grid-template-columns: repeat(3, 1fr);
        }
    }

    @media (min-width: 1200px) {
        .product-grid {
            grid-template-columns: repeat(4, 1fr);
        }
    }
}

/* @supports inside @media */
@media (min-width: 768px) {
    @supports (backdrop-filter: blur(10px)) {
        .sticky-nav {
            background: rgba(255, 255, 255, 0.8);
            backdrop-filter: blur(10px);
        }
    }

    @supports not (backdrop-filter: blur(10px)) {
        .sticky-nav {
            background: rgba(255, 255, 255, 0.95);
        }
    }
}

/* Using with CSS nesting for cleaner organization */
.sidebar {
    width: 100%;
    padding: 1rem;

    @media (min-width: 768px) {
        width: 300px;
        position: sticky;
        top: 2rem;
    }

    @supports (container-type: inline-size) {
        container-type: inline-size;

        .widget {
            @container (min-width: 250px) {
                display: grid;
                grid-template-columns: auto 1fr;
            }
        }
    }
}

/* Combining both with prefers-color-scheme */
@supports (color: oklch(0.5 0.2 240)) {
    @media (prefers-color-scheme: dark) {
        :root {
            --bg: oklch(0.15 0.02 260);
            --text: oklch(0.9 0.02 260);
            --primary: oklch(0.65 0.25 250);
        }
    }

    @media (prefers-color-scheme: light) {
        :root {
            --bg: oklch(0.98 0.01 260);
            --text: oklch(0.2 0.02 260);
            --primary: oklch(0.55 0.2 250);
        }
    }
}

When NOT to Use @supports

While @supports is powerful, there are situations where using it is unnecessary, redundant, or even counterproductive. Knowing when to skip feature queries is just as important as knowing how to use them.

When the Property Degrades Gracefully

Many CSS properties are naturally backwards-compatible. If a browser does not understand a property, it simply ignores the declaration and moves on. In these cases, @supports adds complexity without benefit.

Cases Where @supports Is Unnecessary

/* UNNECESSARY: border-radius degrades gracefully */
/* Old browsers just show square corners */
@supports (border-radius: 8px) {
    .card { border-radius: 8px; }
}
/* JUST WRITE: */
.card { border-radius: 8px; }

/* UNNECESSARY: box-shadow degrades gracefully */
@supports (box-shadow: 0 2px 4px rgba(0,0,0,0.1)) {
    .card { box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
}
/* JUST WRITE: */
.card { box-shadow: 0 2px 4px rgba(0,0,0,0.1); }

/* UNNECESSARY: opacity degrades gracefully */
@supports (opacity: 0.5) {
    .overlay { opacity: 0.5; }
}
/* JUST WRITE: */
.overlay { opacity: 0.5; }

/* UNNECESSARY: transition degrades gracefully */
/* No transition is not a broken experience */
.button {
    transition: background 0.2s ease;
}

/* UNNECESSARY: custom properties when used with fallbacks */
.card {
    color: #333;          /* Fallback */
    color: var(--text);   /* Enhancement */
    /* The fallback line handles unsupported browsers */
}

/* UNNECESSARY: when support is essentially universal */
/* Flexbox is supported by 99%+ of browsers */
@supports (display: flex) {
    .nav { display: flex; }
}
/* JUST WRITE: */
.nav { display: flex; }

When You Should Use @supports

Cases Where @supports Is Valuable

/* VALUABLE: When the enhancement changes the entire layout model */
/* Grid changes layout fundamentally and the fallback needs different CSS */
.container { display: flex; flex-wrap: wrap; }
.container .item { flex: 0 0 calc(33.333% - 2rem); margin: 1rem; }

@supports (display: grid) {
    .container { display: grid; grid-template-columns: repeat(3, 1fr); gap: 2rem; }
    .container .item { margin: 0; } /* Override flex fallback margin */
}

/* VALUABLE: When the fallback needs completely different properties */
@supports not (aspect-ratio: 16 / 9) {
    .video-wrapper {
        position: relative;
        padding-bottom: 56.25%; /* 16:9 padding hack */
        height: 0;
    }
    .video-wrapper iframe {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
    }
}

@supports (aspect-ratio: 16 / 9) {
    .video-wrapper {
        aspect-ratio: 16 / 9;
        width: 100%;
    }
    .video-wrapper iframe {
        width: 100%;
        height: 100%;
    }
}

/* VALUABLE: When testing newer features with limited support */
@supports (grid-template-rows: masonry) {
    /* Masonry layout */
}

@supports selector(:has(*)) {
    /* Parent selection */
}

@supports (animation-timeline: scroll()) {
    /* Scroll-driven animations */
}
Note: The general rule is: use @supports when the fallback requires different CSS properties or layout structures, and skip it when the unsupported property simply has no visual effect (graceful degradation). If removing a single property declaration from your CSS would still produce an acceptable layout, you probably do not need @supports.

Practical Progressive Enhancement Examples

Let us build several real-world examples that demonstrate how to use @supports for progressive enhancement in production code.

Example 1: Modern Navigation Bar

Progressive Navigation Bar

/* Baseline: simple navigation */
.main-nav {
    background: white;
    padding: 1rem 2rem;
    border-bottom: 1px solid #eee;
}

.main-nav .nav-inner {
    max-width: 1200px;
    margin: 0 auto;
}

.nav-links {
    list-style: none;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
}

.nav-links a {
    color: #333;
    text-decoration: none;
    padding: 0.5rem 1rem;
}

/* Enhancement: sticky with backdrop blur */
@supports (position: sticky) {
    .main-nav {
        position: sticky;
        top: 0;
        z-index: 100;
    }
}

@supports (backdrop-filter: blur(10px)) or (-webkit-backdrop-filter: blur(10px)) {
    .main-nav {
        background: rgba(255, 255, 255, 0.85);
        backdrop-filter: blur(10px);
        -webkit-backdrop-filter: blur(10px);
    }
}

/* Enhancement: :has() for active state indication */
@supports selector(:has(*)) {
    .nav-links li:has(a[aria-current="page"]) {
        border-bottom: 2px solid #3498db;
    }

    .main-nav:has(.search-input:focus) {
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    }
}

Example 2: Responsive Image Gallery

Progressive Image Gallery

/* Baseline: simple column layout */
.gallery {
    column-count: 2;
    column-gap: 1rem;
}

.gallery .gallery-item {
    break-inside: avoid;
    margin-bottom: 1rem;
}

.gallery img {
    width: 100%;
    display: block;
    border-radius: 8px;
}

/* Enhancement: grid layout */
@supports (display: grid) {
    .gallery {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
        gap: 1rem;
        column-count: unset;
    }

    .gallery .gallery-item {
        margin-bottom: 0;
    }
}

/* Enhancement: aspect-ratio for consistent sizing */
@supports (aspect-ratio: 4 / 3) {
    .gallery img {
        aspect-ratio: 4 / 3;
        object-fit: cover;
    }
}

/* Enhancement: masonry for dynamic heights */
@supports (grid-template-rows: masonry) {
    .gallery {
        grid-template-rows: masonry;
    }

    .gallery img {
        aspect-ratio: unset;
    }
}

/* Enhancement: container queries for gallery items */
@supports (container-type: inline-size) {
    .gallery .gallery-item {
        container-type: inline-size;
    }

    @container (min-width: 300px) {
        .gallery-caption {
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
    }
}

Example 3: Modern Form Styling

Progressive Form Enhancements

/* Baseline: clean form styles */
.form-field {
    margin-bottom: 1.5rem;
}

.form-field label {
    display: block;
    margin-bottom: 0.5rem;
    font-weight: 600;
    color: #333;
}

.form-field input,
.form-field textarea {
    width: 100%;
    padding: 0.75rem 1rem;
    border: 2px solid #ddd;
    border-radius: 6px;
    font-size: 1rem;
    transition: border-color 0.2s;
}

.form-field input:focus,
.form-field textarea:focus {
    border-color: #3498db;
    outline: none;
}

/* Enhancement: :focus-visible for better UX */
@supports selector(:focus-visible) {
    .form-field input:focus,
    .form-field textarea:focus {
        outline: none;
        border-color: #ddd;
    }

    .form-field input:focus-visible,
    .form-field textarea:focus-visible {
        border-color: #3498db;
        box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
    }
}

/* Enhancement: :has() for parent-aware styling */
@supports selector(:has(*)) {
    .form-field:has(input:focus-visible) label {
        color: #3498db;
    }

    .form-field:has(:invalid:not(:placeholder-shown)) {
        label {
            color: #e74c3c;
        }
    }

    .form-field:has(:valid:not(:placeholder-shown)) label {
        color: #2ecc71;
    }

    /* Show character count only when textarea is focused */
    .form-field .char-count {
        display: none;
    }

    .form-field:has(textarea:focus) .char-count {
        display: block;
    }
}

/* Enhancement: accent-color for native form controls */
@supports (accent-color: auto) {
    input[type="checkbox"],
    input[type="radio"],
    input[type="range"] {
        accent-color: #3498db;
    }
}

Debugging @supports Queries

When your @supports queries are not working as expected, here are techniques for debugging them.

Debugging Techniques

/* 1. Use the browser DevTools to check @supports evaluation */
/* In Chrome DevTools, you can see @supports blocks in the Styles panel */
/* They show as collapsible groups, grayed out if the condition is false */

/* 2. Use JavaScript to quickly test a condition */
console.log('Grid:', CSS.supports('display', 'grid'));
console.log('Subgrid:', CSS.supports('grid-template-columns', 'subgrid'));
console.log(':has():', CSS.supports('selector(:has(*))'));
console.log('Nesting:', CSS.supports('selector(&)'));

/* 3. Use a visible indicator for debugging */
@supports (display: grid) {
    body::after {
        content: "Grid: YES";
        position: fixed;
        bottom: 10px;
        right: 10px;
        background: green;
        color: white;
        padding: 0.5rem 1rem;
        border-radius: 4px;
        font-size: 12px;
        z-index: 99999;
    }
}

@supports not (display: grid) {
    body::after {
        content: "Grid: NO";
        position: fixed;
        bottom: 10px;
        right: 10px;
        background: red;
        color: white;
        padding: 0.5rem 1rem;
        border-radius: 4px;
        font-size: 12px;
        z-index: 99999;
    }
}

/* 4. Common mistakes to check */
/* WRONG: Testing property without value */
@supports (display) { /* Invalid */ }

/* WRONG: Missing parentheses around condition */
@supports display: grid { /* Invalid */ }

/* WRONG: Testing with quotes around value */
@supports (display: "grid") { /* Invalid - grid is a keyword, not a string */ }

/* RIGHT: Complete declaration in parentheses */
@supports (display: grid) { /* Valid */ }
Warning: If a @supports condition has a syntax error (like missing parentheses or an invalid condition), the entire block is ignored silently. The browser will not throw an error or warn you. This makes it important to double-check your @supports syntax carefully. Use browser DevTools to verify that the block is being evaluated correctly.

Exercise 1: Build a Progressive Enhancement Stylesheet

Create a stylesheet for a blog post page that progressively enhances from baseline to cutting-edge CSS. Start with a baseline layout using only display: block and basic typography. Layer 1: Use @supports (display: flex) to create a flexbox-based header with a logo and navigation. Layer 2: Use @supports (display: grid) to create a grid-based layout with a sidebar and main content area. Layer 3: Use @supports (backdrop-filter: blur(10px)) to add a frosted glass sticky header. Layer 4: Use @supports selector(:has(*)) to highlight the table of contents item for the currently visible section. Layer 5: Use @supports (animation-timeline: scroll()) to add a scroll-driven reading progress bar. For each layer, the fallback must be visually acceptable. Test your feature queries using CSS.supports() in the browser console and verify that each layer is independent -- disabling one should not break the others.

Exercise 2: Create a Feature-Aware Component Library

Build three UI components that each use @supports for progressive enhancement. Component 1: A .glass-card that uses backdrop-filter for a frosted glass effect when supported, falling back to a semi-transparent solid background. Include both vendor-prefixed checks with the or operator. Component 2: A .smart-form that uses @supports selector(:has(*)) to style form groups based on their input states (focus, valid, invalid). Without :has() support, use a class-based approach as fallback. Component 3: A .fluid-grid that starts with a flexbox layout, upgrades to grid with @supports (display: grid), and further upgrades to subgrid with @supports (grid-template-columns: subgrid) for perfectly aligned card content. For each component, include a JavaScript snippet using CSS.supports() that logs which enhancement tier the browser is using. Combine @supports with @media queries so the grid component is single-column on mobile regardless of grid support.