Understanding @supports
The @supports
rule-block in CSS allows us
to “test” a bit of CSS,
to find out if the browser understands it.
If the test condition is supported (true
),
the browser will continue to parse any CSS
inside the rule-block.
If the condition is not supported (false
),
the entire block is ignored.
The basic syntax only allows us to test
isolated property-value pairs:
@supports (grid-template-columns: subgrid) {
.grid-item { margin: 0; }
}
@supports (--css: vars) {
button { background: var(--button); }
}
Since CSS is already resilient,
allowing browsers to ignore code they don’t understand,
we can often use new features without any explicit test.
The @supports
rule is only necessary
when we need support for one property
to impact how we use other properties –
like changing a margin based on support for subgrid.
It can also be useful to test for lack of support,
by adding a “negation” (not
) to our test:
@supports not (--css: vars) {
button { background: rebeccapurple; }
}
Adding new support tests
When new properties and values are added to CSS,
browsers don’t need to update anything –
the new tests can be written in the existing syntax,
and even legacy browsers give us the proper answer
(as long as they understand the basics of @supports
).
But sometimes we need to test CSS features
that are not based on a property/values pair.
Over the last couple years,
browsers have mostly implemented a syntax
for testing support on selectors:
@supports selector(::marker) {
}
@supports not selector(::marker) {
}
I covered that in more detail
with a video about selector support queries
back in 2019.
Now we’re also planning to add
a new syntax for testing
if container queries are supported:
@supports container(min-width: 1em) {
}
@supports not container(min-width: 1em) {
}
This will also allow us to test
potential new queries
as they get added to the specification.
Over time,
I imagine CSS will continue to add
even more features that need testing,
and some of those will require new testing syntax.
We should be planning ahead to make sure those new features,
and new feature-tests,
work as expected in our current browsers.
Unknown feature or unknown test?
Maybe you’ve already noticed the problem.
In order to signal support,
the browser has to understand both
the feature being tested,
and the syntax of the test.
When we use new syntax to test for support of a new feature,
we’re actually testing for support of both at once.
Because of the resilience mentioned above,
browsers that don’t understand the new test syntax
will also give us a negative result (no support)
and skip the code-block in question,
even if they do support the new feature.
For some time now,
WebKit browsers (like Safari) have had support
for the ::marker
pseudo-element selector,
but do not yet support the @supports selector()
syntax.
Several versions of Safari
would give us a false negative
when we test for support of the ::marker
selector,
even though they understand it just fine.
The problem isn’t lack of support for the selector,
but lack of support for the new @supports
syntax.
(The relevant bug has been closed,
and the selector()
syntax now works in
Safari Technology Preview,
so I expect this will be out-of-date soon.)
While there’s some potential for a false-negative,
browsers all agree
on how to handle this situation…
at least on the surface.
Behind the scenes
they are doing something slightly different,
which we’ll see in a minute.
Negating the unknown
With both selector()
and container()
,
we’re testing for support of “wrapping” features:
Selectors wrap around any number of declarations,
and the @container
query rule wraps around
any number of selector rule-blocks.
Since browsers ignore blocks of code
that they don’t understand,
these wrapping features often act as their own
@supports
conditional test:
.cat-item::marker {
content: '😻';
}
@container (min-width: 60ch) {
main { display: grid; }
}
In these cases,
it is much more useful to test
for lack of support,
and provide a fallback value.
We want to use the negation syntax:
@supports not selector(::marker) {
.cat-item { list-style: none; }
.cat-item::before { content: '😻';}
}
@supports not container(min-width: 60ch) {
@media (min-width: 50em) {
main { display: grid; }
}
}
This is where browsers disagree,
and we start to see the implications
around how they handle unknown syntax internally:
- Some browsers consider unknown syntax
to mean lack of support, a value of
false
- Other browsers consider unknown syntax to be… unknown.
They return a value of
unknown
Those two values,
false
and unknown
,
behave the same when we’re testing basic support –
both of them act like false
.
But these values behave very differently
when we negate them:
not false
is the same as true
not unknown
is… still just unknown
(acts like false
again)
You can’t negate the unknown.
The path forward
When it comes to testing support for new features,
like Container Queries,
we really want the ability to add new syntax,
and then write negative tests of that syntax,
and get the not false
(true
) result in old browsers.
I know this is a lot of double-negatives,
so let’s look at it in context:
@supports not container(min-width: 1em) {
}
I opened an issue
with the CSS Working Group,
where Oriol Brufau pointed out
that the answer depends on
exactly how we ask the question.
Specifically, we can get different results
by adding/removing function arguments,
or wrapping our query in parenthesis.
I’ve expanded on his table here
to show the different results across browsers.
You can see the live result from your current browser,
along with known results from the other major browsers.
I’m using value
and fn()
as placeholders
for “unknown tests” –
since neither of these are supported anywhere:
@supports … |
Live Result |
Firefox |
Chromium |
WebKit |
not (value) |
true
false
|
true |
true |
true |
not fn() |
true
false
|
true |
false |
false |
not fn(value) |
true
false
|
true |
true |
false |
not (fn()) |
true
false
|
true |
false |
true |
not (fn(value)) |
true
false
|
true |
true |
true |
Again,
what we want to see is true
across the board.
That would mean we can add future support for
unknown features (e.g. value
and fn()
),
and have old browsers acknowledge their lack of support.
Earlier this week,
the CSS Working Group resolved that
unknown @supports
expressions
should evaluate to false
for all @supports
rules –
which is good news moving forward.
But the table above
suggests that we also have a temporary solution
we can use right away.
The conclusion
While we wait for browsers to standardize
how they handle negation of unknown support,
developers should wrap any new test syntax in parentheses:
@supports not (container(min-width: 1em)) {
}
Any legacy browser that understands @supports
will parse and apply the CSS inside this block.
Then, as browsers start to implement container queries,
they will also implement the new testing syntax –
hiding the fallback for you.
🥳 Migration path achieved!
🎉 Progressive enhancement!
Cover photo by
Neil Thomas
on Unsplash.