Complete CSS Course Module 16: Advanced: Cutting-Edge CSS

Feature Queries with @supports and Progressive Enhancement

  • Beginner
  • Lesson 16.1
  • 4 min read
  • 67 of 77 in the course

In short

CSS @supports lets you check if a browser understands a feature before you use it. Learn its syntax, how to write fallbacks first, and the idea of progressive enhancement. What is @supports in CSS? @supports is a feature query.

CSS @supports lets you check if a browser understands a feature before you use it. Learn its syntax, how to write fallbacks first, and the idea of progressive enhancement.

What is @supports in CSS?

@supports is a feature query. It asks the browser a question: "Do you understand this property and value?" If the answer is yes, the CSS inside the block is used. If not, the browser skips the whole block. It works like a media query, but instead of checking the screen size, it checks the browser's abilities.

@supports (display: grid) {
  .gallery {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 16px;
  }
}

The gallery becomes a grid only in browsers that support Grid (today, all of them). The test inside the brackets must be a full declaration: a property, a colon and a value.

Note: Browsers don't need @supports to stay safe. If a browser doesn't understand a declaration, it simply ignores that one line. @supports is useful when several rules only make sense together, or when the fallback needs different styles.

Combining tests with not, and, or

You can build more complex questions with three keywords:

  • not – the feature is not supported. Great for writing a fix only for older browsers.
  • and – both features must be supported.
  • or – at least one feature is supported.
@supports not (aspect-ratio: 1 / 1) {
  .video-box {
    height: 0;
    padding-top: 56.25%;
  }
}

@supports (position: sticky) and (backdrop-filter: blur(4px)) {
  .site-header {
    position: sticky;
    top: 0;
    background: rgb(255 255 255 / 0.7);
    backdrop-filter: blur(4px);
  }
}

@supports (text-wrap: balance) or (text-wrap: pretty) {
  .title {
    max-width: none;
  }
}

The first block uses the old "padding hack" only in browsers without aspect-ratio. The second makes a see-through, blurred sticky header only when both features work, because a see-through header without blur would be hard to read. The third removes a manual width limit when the browser can balance lines by itself. When you mix and and or, always use brackets to group them.

Testing selectors with selector()

Sometimes you need to know whether a selector works, not a property. selector() does that. This is very useful for newer selectors like :has().

.card {
  border: 1px solid #e5e7eb;
}

@supports selector(:has(img)) {
  .card:has(img) {
    display: grid;
    grid-template-columns: 120px 1fr;
    gap: 16px;
  }
}

Cards that contain an image get a two-column layout in browsers that support :has(). In other browsers, the cards just stay simple boxes with a border, which is perfectly fine. Without the test, an unsupported selector would make the browser drop the whole rule anyway, but the feature query makes your intention clear and lets you group several related rules.

Progressive enhancement: write the fallback first

Progressive enhancement means: build a version that works everywhere first, then add extras for browsers that can do more. Think of a house: first the walls and the roof (everyone needs those), then the nice paint and the garden lights. If a browser is old, users still get a working house.

In CSS, this means you write the simple styles at the top, and the modern styles after them or inside @supports. The cascade does the rest: later rules win, and unknown rules are ignored.

.button {
  background: #2563eb;
  background: oklch(55% 0.2 260);
}

.hero-title {
  font-size: 2.5rem;
  font-size: clamp(2rem, 5vw, 4rem);
}

Each property is written twice. Every browser understands the first line. Modern browsers also understand the second line, and because it comes later, it wins. An old browser doesn't understand oklch() or clamp(), ignores that line, and keeps the first value. Nobody gets a broken page.

Here is a bigger real-world example: a layout that uses modern container queries but still looks good without them.

.product {
  display: block;
}

.product img {
  width: 100%;
}

@supports (container-type: inline-size) {
  .product-list {
    container-type: inline-size;
  }

  @container (min-width: 500px) {
    .product {
      display: flex;
      gap: 16px;
    }

    .product img {
      width: 200px;
    }
  }
}

Every browser shows the products stacked, with full-width images. Browsers that support container queries switch to a side-by-side layout when there is enough room.

Common mistake: Writing the modern version first and the fallback after it. The fallback comes later, so it wins in every browser, and nobody sees your modern styles. Fallback first, enhancement second.

Tip: @supports checks whether the browser parses the feature, not whether it is fully bug-free. Always test your page in at least two different browsers. Check browser support on MDN's @supports page.

Practice exercise

Style a "testimonials" section. First, give the quote cards a simple stacked layout and a solid background. Then, inside @supports (display: grid) and (gap: 1rem), turn them into a three-column grid. Finally, add a block with @supports not (backdrop-filter: blur(1px)) that makes a frosted header fully solid.

Hint: open DevTools and change a property name in the test (for example to displayy) to see the fallback version.

Key takeaways

  • @supports (property: value) applies CSS only when the browser understands it.
  • Combine tests with not, and and or, and use brackets to group them.
  • selector() tests whether a selector such as :has() is supported.
  • Progressive enhancement: a working base for everyone, extras for modern browsers.
  • Always write the fallback before the modern version.