CSS has a neat feature that allows us to test if the browser supports a particular property or property:value combination before applying a block of styles — like how a @media
query matches when, say, the width of the browser window is narrower than some specified size and then the CSS within it takes effect. In the same spirit, the CSS inside this feature will take effect when the property:value pair being tested is supported in the current browser. That feature is called @supports
and it looks like this:
@supports (display: grid) {
.main {
display: grid;
}
}
Why? Well, that’s a bit tricky. Personally, I find don’t need it all that regularly. CSS has natural fallback mechanisms such that if the browser doesn’t understand a property:value combination, then it will ignore it and use something declared before it if there is anything, thanks to the cascade. Sometimes that can be used to deal with fallbacks and the end result is a bit less verbose. I’m certainly not a it’s-gotta-be-the-same-in-every-browser kinda guy, but I’m also not a write-elaborate-fallbacks-to-get-close kinda guy either. I generally prefer a situation where a natural failure of a property:value doesn’t do anything drastic to destroy functionality.
That said, @supports
certainly has use cases! And as I found out while putting this post together, plenty of people use it for plenty of interesting situations.
A classic use case
The example I used in the intro is a classic one that you’ll see used in lots of writing about this topic. Here it is a bit more fleshed out:
/* We're gonna write a fallback up here in a minute */
@supports (display: grid) {
.photo-layout {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-gap: 2rem;
}
}
Nice grid! Repeating and auto-filling columns is a sweet feature of CSS grid. But, of course, there are browsers that don’t support grid, or don’t support all the specific features of it that I’m using above there.
For example, iOS shipped support for CSS grid in version 10.2, but iOS has had flexbox support since version 7. That’s a decent gap of people with older iOS devices that do support flexbox but not grid. I’m sure there are more example gaps like that, but you probably get the idea.
I was running on an older version of mobile safari and many many many many many sites were flat out broken that used grid
I’m waiting another year or so before messing about with it
— David Wells (@DavidWells) February 6, 2019
It may be acceptable to let the fallback for this be nothing, depending on the requirements. For example, vertically stacked block-level elements rather than a multi-column grid layout. That’s often fine with me. But let’s say it’s not fine, like a photo gallery or something that absolutely needs to have some basic grid-like structure. In that case, starting with flexbox as the default and using @supports
to apply grid features where they’re supported may work better…
.photo-layout {
display: flex;
flex-wrap: wrap;
> div {
flex: 200px;
margin: 1rem;
}
}
@supports (display: grid) {
.photo-layout {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-gap: 2rem;
> div {
margin: 0;
}
}
}
The “fallback” is the code outside of the @supports
block (the properties above the block in the example above), and the grid code is either inside or after. The @supports
block doesn’t change any specificity, so we need the source order to help make sure the overrides work.
Notice I had to reset the margin on the divs inside the @supports
block. That’s the kind of thing I find a bit annoying. There is just enough crossover between the two scenarios that it requires being super aware of how they impact each other.
Doesn’t that make you wish it could be entirely logically separated…
There is “not” logic in @supports blocks, but that doesn’t mean it should always be used
Jen Simmons put this example in an article called Using Feature Queries in CSS a few years ago:
/* Considered a BAD PRACTICE, at least if you're supporting IE 11 and iOS 8 and older */
@supports not (display: grid) {
/* Isolated code for non-support of grid */
}
@supports (display: grid) {
/* Isolated code for support of grid */
}
Notice the not
operator in the first block. That’s checking for browsers that do not support grid in order to apply certain styles to those browsers. The reason this approach is considered bad practice is that the browser support for @supports itself has to be considered!. That’s what makes this so dang tricky.
It’s very appealing to write code in logically separated @supports
blocks like that because then it’s starting from scratch each time and doesn’t need to be overriding previous values and dealing with those logical mind-benders. But let’s go back to the same iOS situation we considered before… @supports
shipped in iOS in version 9 (right between when flexbox shipped in iOS 7 and grid shipped in iOS 10.2). That means any flexbox fallback code in a @supports
block using the not
operator to check for (display: grid) {}
support wouldn’t work in either iOS 7 or 8, meaning the fallback now needs a fallback from working in browsers it otherwise would have. Phew!
The big reason to reach for @supports
is to account for very different implementations of something depending on feature support where it becomes easier to reason and distinguish between those implementations if the blocks of code are separated.
We’ll probably get to a point where we can use mutually-exclusive blocks like that without worry. Speaking of which…
@supports is likely to be more useful over time.
Once @supports
is supported in all browsers you need to support, then it’s good to start using it more aggressively and without having to factor in the complication of considering whether @supports
itself is supported. Here’s the support grid on that:
This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.
Desktop
Chrome | Firefox | IE | Edge | Safari |
---|---|---|---|---|
28 | 22 | No | 12 | 9 |
Mobile / Tablet
Android Chrome | Android Firefox | Android | iOS Safari |
---|---|---|---|
126 | 127 | 4.4 | 9.0-9.2 |
Basically, IE 11 and any iOS device stuck on iOS 8 are the pain points. If your requirements are already past those, then you’re good to use @supports
more freely.
The irony is that there hasn’t been a ton of CSS features shipping that have big clear @supports
use cases — but there are some! Apparently, it’s possible to test new fancy stuff like Houdini:
Using it on my wedding website to check for Houdini support 🎩🐰
— Sam Richard (@Snugug) February 6, 2019
(I’m not sure entirely what you’d put in the @supports
block to do that. Has anyone else done this?)
When @supports isn’t doing anything useful
I’ve seen a good amount of @supports
uses in the wild where the end result is exactly as it would be without using it. For example…
@supports (transform: rotate(5deg)) {
.avatar {
transform: rotate(5deg);
}
}
On some level, that makes perfect logical sense. If transforms are supported, use them. But it’s unnecessary if nothing different is happening in a non-support scenario. In this case, the transform can fail without the @supports
block and the result is the same.
Here’s another example of that shaking out.
There are browser extensions for playing with @supports
There are two of them!
- Feature Queries Manager by Ire Aderinokun
- CSS Feature Toggle Extension by Keith Clark
They are both centered around the idea that we can write @supports
blocks in CSS and then toggle them on and off as if we’re looking at a rendering of the code in a browser that does or doesn’t support that feature.
Here’s a video of Keith’s tool applied to the scenario using grid with a flexbox fallback:
Ire’s tool, which she wrote about in the article Creating The Feature Queries Manager DevTools Extension, has a slightly different approach in that it shows the feature queries that you actually wrote in your CSS and provides toggles to flip them on and off. I don’t think it works through iframes though, so I popped open Debug Mode to use it on CodePen.
More real world use cases for @supports
Here’s one from Erik Vorhes. He’s styling some custom checkboxes and radio buttons here, but wraps all of it in a @supports
block. None of the styling gets applied unless the block passes the support check.
@supports (transform: rotate(1turn)) and (opacity: 0) {
/* all the styling for Erik's custom checkboxes and radio buttons */
}
Here are several more I’ve come across:
- Joe Wright and Tiago Nunes mentioned using it for
position: sticky;
. I’d love to see a demo here! As in, where you go forposition: sticky;
, but then have to do something different besides let it fail for a non-supporting browser. - Keith Grant and Matthias Ott mention using it for
object-fit: contain;
. Matthias has a demo where positioning trickery is used to make an image sort of fill a container, which is then made easier and better through that property when it’s available. - Ryan Filler mentions using it for
mix-blend-mode
. His example sets more opacity on an element, but ifmix-blend-mode
is supported, it uses a bit less and that property which can have the effect of seeing through an element on its own.
.thing {
opacity: 0.5;
}
@supports (mix-blend-mode: multiply) {
.thing {
mix-blend-mode: multiply;
opacity: 0.75;
}
}
backdrop-filter
property. He says, “when it’s supported the opacity of the background color often needs some fine tuning.”@supports (-ms-ime-align:auto) { }
.clip-path
because adjusting the sizing or padding of an element will accommodate when clipping is unavailable.initial-letter
CSS property that’s pretty fantastic for drop caps, but is used in conjunction with other properties that you may not want to apply at all if initial-letter
isn’t supported (or if there’s a totally different fallback scenario).Here’s a bonus one from Nick Colley that’s not @supports
, but @media
instead! The spirit is the same. It can prevent that “stuck” hover state on touch devices like this:
@media (hover: hover) {
a:hover {
background: yellow;
}
}
Logic in @supports
Basic:
@supports (initial-letter: 4) {
}
Not:
@supports not (initial-letter: 4) {
}
And:
@supports (initial-letter: 4) and (transform: scale(2)) {
}
Or:
@supports (initial-letter: 4) or (-webkit-initial-letter: 4) {
}
Combos:
@supports ((display: -webkit-flex) or
(display: -moz-flex) or
(display: flex)) and (-webkit-appearance: caret) {
}
JavaScript Variant
JavaScript has an API for this. To test if it exists…
if (window.CSS && window.CSS.supports) {
// Apparently old Opera had a weird implementation, so you could also do:
// !!((window.CSS && window.CSS.supports) || window.supportsCSS || false)
}
To use it, either pass the property to it in one param and the value in another:
const supportsGrid = CSS.supports("display", "grid");
…or give it all in one string mirroring the CSS syntax:
const supportsGrid = CSS.supports("(display: grid)");
Selector testing
At the time of this writing, only Firefox supports this sort of testing (behind an experimental flag), but there is a way to test the support of selectors with @supports
. MDN’s demo:
@supports selector(A > B) {
}
You?
Of course, we’d love to see Pens of @supports
use cases in the comments. So share ’em!
An example with Houdini would be: background-image: paint(checkerboard);
I imitate
@supports selector()
like this:I wonder if PostCSS changes the real syntax to something like that…
Thanks for this great summary. About Houdini and
@supports
, here is few examples I made while creating https://css-houdini.rocksAdding
padding
when painting a slanted background (https://css-houdini.rocks/slanted-backgrounds)Adding
appearance: none
when painting own customs checkboxes (https://css-houdini.rocks/checkboxes)Removing fallback background when painting irregular one (applied as
border-image
+fill
) (https://css-houdini.rocks/rough-boxes)Resetting
border-radius
when painting smooth corners, aka squircle (https://css-houdini.rocks/smooth-corners)Resetting
overflow
when painting irregular mask painted asborder-image
+outset
(https://css-houdini.rocks/irregular-grid)All demos have code sample :)
I think some versions of IE also support the “support”.
They don’t though. Edge, yes; IE, no.
Here’s a demo of the drop cap example mentioned in my tweet: https://codepen.io/stacy/pen/mORmWo
WRT “Joe Wright and Tiago Nunes mentioned using it for position: sticky”…
A primary reason is when possibly sticky elements must be (and are always) containing blocks for absolute positioned descendants. You can’t have it just fallback to position: static, because descendants would lose that containing block. So you need it positioned, most likely as relative, but box offset values, like top: 50px, don’t fallback nicely from sticky to relative schemes.
So I commonly have…
…and this is all aside from the js complex sticky/fixed polyfilling that folks deploy.
I’ve been using
@supports
quite a lot for extending CSS with styles that need to be supported by JavaScript plugins. It turns out there’s quite an overlap between what you can store in CSS and successfully parse, and the sort of information you need to pass to styling-related plugins.For me, a common example might be something like
You can read more about the idea of using
@supports
this way from this README on Github or view some more examples in this post on CodePenThis is very cool article! I published one about the same subject more than 3 years ago and I came to the same conclusions back then — wait for the support for @supports.
https://pawelgrzybek.com/native-feature-detection-with-csssupports-api/
Here’s an example of using
@supports
to detect CSS feature using the browser that you are currently in use:Safari, both mobile and desktop, has issues detecting support for css variables. Had to use a variation of this gist to let Apple users into my wedding website :-/
https://gist.github.com/wesbos/8b9a22adc1f60336a699
Hi, nice content!
So i was thinking in a case cenario:
Supose that i need to support IE 10 and can’t use display flex or grid.
Why should i ever write something inside a supports bracket if i already have to support an old browser (using float or display inline-block) and write a code that already works in every browser
What about the case when we customize
<select>
? In this case case you want to remove default arrow on the right and add your own SVG icon. But you better make sure that will work and IE won’t get both arrows.select {
border: 1px solid purple;
background: pink;
...
}
@supports (-webkit-appearance: none) or (-moz-appearance: none) {
select {
-webkit-appearance: none;
-moz-appearance: none;
padding-right: 30px;
background: url("arrow.svg");
}
}
Anton Shchukin it was a reply to me?
Anyway, in this case you would only style if it supports?
I think that the best solution is to insert the image overlaying the default browser arrow so it will be fine in all browsers.
It is always centered and at the right so you can insert by background, or even with after pseudo selector or with absolute in the select on a div wrapping it