How do we plan to handle inconsistencies with the new CSS Nesting specification?
See original GitHub issueThe CSS Nesting specification is moving quickly towards a First Public Working Draft, and potential implementation. We have some time before it will stabilize and land in browsers, but there is a lot of syntax overlap with some key differences in output. I wanted to document some of the ways it will conflict with (and give different results compared to) Sass nesting.
The spec is based very much on our feature, using the same &
syntax, but adjusted to ensure that browsers only need single-character look-ahead. That means:
- Every nested selector requires the
&
parent reference - Our syntax is supported directly when the nested selector starts with
&
Sass-only
There are a few use cases we support, but CSS does not:
.block {
p { … } /* CSS requires the implied & prefix for a descendant combinator */
main & { … } /* CSS requires & to be the first character for this syntax */
&_element { … } /* CSS does not support string-concatenation */
}
CSS-only
CSS provides a second syntax that has no current meaning with Sass. The @nest
rule allows more complex combinations, while still avoiding infinite-lookahead.
/* required in cases such as… */
.block {
@nest main & { … }
@nest :not(&) { … }
}
/* optional in some cases… */
.block {
@nest & p { … }
@nest &.active { … }
}
CSS/Sass overlap
Then there are some rules supported by both Sass & CSS syntax, such as:
.foo, .bar {
color: blue;
& + .baz, &.qux { color: red; }
}
.foo {
color: blue;
& .bar & .baz & .qux { color: red; }
}
While the syntax is the same, these cases are interpreted in slightly different ways. Sass generates all the possible combinations:
.foo, .bar { color: blue; }
.foo + .baz,
.foo.qux,
.bar + .baz,
.bar.qux { color: red; }
.foo { color: blue; }
.foo .bar .foo .baz .foo .qux { color: red; }
While CSS relies on the :is()
selector for a simpler reading:
.foo, .bar { color: blue; }
:is(.foo, .bar) + .baz,
:is(.foo, .bar).qux { color: red; }
.foo { color: blue; }
.foo .bar .foo .baz .foo .qux { color: blue; }
In those examples the output has a different structure, but generally the same meaning – both in terms of matching and specificity. But there are some cases where the specificity is significantly different. The :is()
pseudoclass takes on the highest specificity of any selector inside it, even if that selector is not matched. Compiled selector lists, on the other hand, take the specificity of the highest matching selector. For example:
.error, #e404 {
& > .baz { color: red; }
}
In CSS, this is equivalent to:
:is(.error, #e404) > .baz /* specificity: [1, 1, 0] */
{ color: red; }
While Sass generates:
.error > .baz, /* specificity: [0, 2, 0] */
#e404 > .baz /* specificity: [1, 1, 0] */
{ color: red; }
Thoughts…
We don’t generally add internal functionality around new CSS features before they have been implemented by several browsers – but the overlapped-syntax-with-different-meaning use-cases could cause a problem here.
- Uses of
@nest
can likely be passed through without modifications- It might be possible short-term to require
@nest
as a marker for accessing the native CSS feature? - Long-term we will need to define how this syntax integrates (or doesn’t) with our selector functions
- It might be possible short-term to require
- At some point we’ll need to change our output to use
:is()
or pass-through/generate the appropriate CSS nesting syntax- In some cases that can happen smoothly without any change for authors
- In some cases that will be a specificity-breaking change
- For Sass-only syntax, we have to decide what CSS is generated in each case
- Implicit ancestor
&
could be added to the output or compiled according to the spec - Non
&
-prefixed selectors could add@nest
to the output, or be compiled according to spec - We could continue to compile concatenation basically as-is?
- Implicit ancestor
I’ve likely missed some things here, but thought it was worth starting the conversation.
Issue Analytics
- State:
- Created 2 years ago
- Reactions:16
- Comments:7 (3 by maintainers)
Top GitHub Comments
This is certainly a complex and difficult issue. Generally speaking, I support trying to converge on the specified CSS semantics, but even generating
:is()
selectors is fraught—many browsers still don’t support it at all, any many more have only partial prefixed support with likely incorrect specificity rules. So we’re stuck between a rock and a hard place: we can either support the newest CSS semantics and break compatibility with older browsers; or we can continue to support our existing semantics and be subtly incompatible with standard CSS.I think we should follow a two-phase plan:
For the time being, we keep the Sass semantics as-is. As you point out, users can opt into the latest CSS behavior using
@nest
which we will always pass through as-is. It is unfortunate to violate our CSS-superset policy here, but preserving backwards-compatibility for such a widely-used feature is more important.Once more than 98% of the global browser market supports unprefixed
:is()
(as per our browser compatibility policy), switch&
to generate:is()
rather than multiple different selectors. Ideally, we can align this with a 2.0.0 release, but that may or may not be realistic based on timings.Assuming the CSSWG moves forward with Option 3, we’re in a tricky situation because it doesn’t provide an obvious way for users to explicitly signal “I intend to use plain CSS syntax here”. @mirisuzanne and @stubbornella, let’s chat about this next time we meet.