Skip to content
164 changes: 11 additions & 153 deletions AtRuleFeatureDetection/explainer.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,18 @@ There have been many scenarios described that call for feature detection of at-r

Allow authors to feature-detect newly introduced at-rules.

Allow authors to feature-detect new enhancements to existing at-rules, such as:
- New media query features and other additions to at-rule preludes
- New descriptors that may be introduced to rules such as `@font-face`

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than deleting these, should we move them to Non-Goals, with a note that the CSSWG resolved to introduce @supports-condition to handle them? And perhaps a link to the spec: https://drafts.csswg.org/css-conditional-5/#supports-condition-rule

At-rule feature detection should be available in all contexts where CSS allows conditioning based on support
of a feature. This includes, but is not limited to,
`@supports`, `CSS.supports()`, `@import ... supports()`, and `@when supports()`.

### Non-goals

- Allow authors to feature-detect new enhancements to existing at-rules, such as:
- New media query features and other additions to at-rule preludes
- New descriptors that may be introduced to rules such as `@font-face`
- Detect non at-rules like `@charset`:

#### CSS `@charset`
The CSS `@charset` rule, despite its appearance, is
[not an at-rule](https://drafts.csswg.org/css-syntax/#charset-rule).
Rather, `@charset` is a marker that can appear only as the first few bytes of a stylesheet file. It signals to
Expand All @@ -82,13 +84,16 @@ CSS is to use UTF-8.

Accordingly, this explainer does not propose making `@charset` feature-detectable using `at-rule()`.

#### Context Aware feature detection
As [mentioned before](#detect-whether-an-at-rule-name-is-recognized-at-all), the `at-rule()` feature returns true if the at-rule name is recognised in any context. This introduces the risk of false positives. As per CSSWG resolutions [#12622](https://github.com/w3c/csswg-drafts/issues/12622) and [#6966](https://github.com/w3c/csswg-drafts/issues/6966#issuecomment-3205037703) the new `@supports-condition` at-rule is introduced as a way to define and name complex support queries, including the ones that need to account for context.

## Proposed Approach

The `at-rule()` function can be used for feature detection in the following ways:
The `at-rule()` function can be used for feature detection in the following way:

### Detect whether an at-rule name is recognized at all

In its simplest form, the `at-rule()` function can be passed just an at-rule name.
The `at-rule()` function can be passed just an at-rule name.
The result is true if the implementation would recognize it as an at-rule in any context, false otherwise.
This form is useful for detecting entire new features implemented as at-rules, including features such as
[`@starting-style`](https://www.w3.org/TR/css-transitions-2/#defining-before-change-style)
Expand Down Expand Up @@ -116,153 +121,6 @@ that do not appear at top-level stylesheet context.
}
```

It may also be useful as a shorter alternative to the second form for feature-detecting at-rules that are only
valid when nested inside another at-rule, such as
[`@swash`](https://www.w3.org/TR/css-fonts/#font-feature-values-syntax)
and other font feature value types within `@font-feature-values`.

```css
@supports at-rule(@swash) {
@font-feature-values Foo {
@swash { pretty: 1; cool: 2; }
}
p {
font-family: Foo;
font-variant-alternates: swash(cool);
}
}
@supports not(at-rule(@swash)) {
/* Fall back to something else. */
@font-face FooButNotAsCool {
/* ... */
}
p {
font-family: FooButNotAsCool;
}
}
```

However, authors should consider the possibility of such an at-rule later becoming valid in a new and different
context, which may result in a false positive. For example, one might write `@supports at-rule(@top-left)`
intending to detect support for the `@top-left` rule nested within `@page`. But later, if a new feature comes
along that implements a nested `@top-left` at-rule for a different purpose, the feature query would return true
on implementations that *do* support this new feature but *do not* support `@page`.

### Detect whether an at-rule, with optional prelude and/or block, is supported

This form resembles existing use of feature detection in CSS. An at-rule block, including optional prelude
and/or declaration block, is passed as the function parameter. If the at-rule block is accepted by the
implementation *without relying on
[forgiving catch-all grammar](#special-case-the-forgiving-grammar-of-media-queries)*,
the support query returns true; otherwise it returns false.

This is useful for detecting new enhancements to existing at-rules. Often it is not necessary to pass the entire
at-rule that the author intends to use; an abbreviated subset can be "close enough" to make the right decision.

```css
/* Are style queries supported at all? */
@supports at-rule(@container style(color: green)) {
/* Yes - use them to emphasize green cards. */
@container style(color: green) and not style(background-color: red) {
.card {
font-weight: bold;
transform: scale(2);
}
}
}
```

The parsing test is performed as if the given at-rule block were the first and only content in a stylesheet.
That allows for at-rules with positional requirements, such as `@import`, to be tested:

```css
/* Import styles into a named layer. */
/* If the implementation does not support layer(), this at-rule will be ignored. */
@import "layered-styles.css" layer(my-layer);

/* If the implementation does not support layers, fall back to unlayered styles. */
@import "unlayered-styles.css" supports(not(at-rule(@import "test.css" layer(test))));
```

#### Special case: The forgiving grammar of media queries

"Forgiving catch-all grammar" refers to cases where it would be undesirable for an entire at-rule to be
thrown away just because a small part of it is unrecognized. One example is in media queries:

```css
@media (max-width: 800px and not(fancy-display)) {
/* ... */
}
```

The implementation does not recognize the `fancy-display` media feature. But since the author is testing that
the page is *not* on a fancy display, we still want the rules within the `@media` block to apply. The media query
handles this by returning an "unknown" value for `fancy-display`, which gets treated as "false" for the purpose
of evaluating the query.

Suppose an author instead wants to condition part of their stylesheet on whether an implementation recognizes
`fancy-display` *at all*. Implementations that recognize `fancy-display`, and implementations that don't, both
must at least parse the above media query. If feature detection were determined purely based on whether the
at-rule parses successfully, it would not be possible to feature-detect support for `fancy-display`. The
exception we carve out allows us to handle this situation: Implementations that recognize `fancy-display` will
parse that feature name on their "known media features" path, and implementations that don't recognize it will
parse it on their "forgiving catch-all" path.

```css
/* This @supports query returns true if the implementation knows what a fancy-display is. */
@supports at-rule(@media (fancy-display)) {

/* This @media query returns true if the user is actually using a fancy-display. */
@media (max-width: 800px and fancy-display) {
/* ... */
}

/* This @media query returns true if the user is detectably not using a fancy-display. */
@media (max-width: 800px and not(fancy-display)) {
/* ... */
}
}
```

### Detect whether a given declaration is supported within an at-rule block

This form allows testing support for descriptors or property declarations within a given at-rule.
Given an at-rule name and a declaration, the support query returns true if the declaration parses within
the at-rule's block, false otherwise.

```css
@supports at-rule(@property; syntax: "<color>") {
/* declare custom properties with color syntax */
}

@supports at-rule(@position-try; box-shadow: 0px 0px) {
/* declare rules that set different box-shadow locations for different position-areas */
}
```

Such tests could also be accomplished with the previous form, but this form allows authors to omit the prelude
and/or required declarations that are not the subject of the test.
For example, the `@font-face` rule requires `font-family` and `src` descriptors.
An author who wants to feature detect support for `font-feature-settings` using the previous form would need
to supply dummy values for `font-family` and `src` in order for the test block to parse successfully.
The query would thus be quite verbose:

```css
/* Testing as a full at-rule using the previous form */
@supports at-rule(@font-face {font-family: test; src: local(test); font-feature-settings: "hwid"} ) {
/* ... */
}
```

With this form, the author can simplify to:

```css
/* Simpler query using this form */
@supports at-rule(@font-face; font-feature-settings: "hwid") {
/* ... */
}
```

## Accessibility, Privacy, and Security Considerations

No accessibility, privacy, or security considerations have been identified for this feature.
Expand Down