A story of
ice, fire
& CSS Nesting

By Lea Verou (@LeaVerou)

CSS Preprocessors

Sass

sass-lang.com
Our story begins back in the ancient 2006, with the appearance of the first CSS preprocessor that actually became popular. It was called Sass, and it was written in Ruby. Two more appeared in the following years: Less, written in JavaScript, and Stylus, also written in JavaScript. CSS Preprocessors are languages that compile to CSS, allowing easier syntax without it having to be suppprted in browsers. There were other preprocessors before Sass, but they didn't really take off. A lot changed in the CSS landscape once preprocessors appeared.

Top 3 preprocessor features

Note that all three features are about reuse: variables and calculations are about value reuse, nesting is selector reuse. This is what CSS was desperately lacking: reusability mechanisms to make it more maintainable.

Sass Variables


				$color-accent: oklch(50% .3 180);
				$font-sans: Helvetica, sans-serif;

				body {
					font: 100%/1.5 $font-sans;
					accent-color: $color-accent;
				}
			

				body {
					font: 100%/1.5 Helvetica, sans-serif;
					accent-color: oklch(.5 .3 180);
				}
			
Preprocessor variables (like every preprocessor feature) are compiled to CSS, which is then sent to the browser.

Native CSS Variables


		

				:root {
					--font-sans: Helvetica, sans-serif;
					--color-accent: oklch(.5 .3 180);
				}

				body {
					font: 100%/1.5 var(--font-sans);
					accent-color: var(--color-accent);
				}
			
At first glance, the CSS version looks more clumsy and verbose. Why not just adopt one of the preprocessor syntaxes?

CSS Variable Reactivity


			.container {
				--accent-color: oklch(60% .15 var(--angle, 0deg));
			}
			h1, button { color: var(--accent-color); }
			button {
				border: .1em solid var(--accent-color);
			}
			button.primary, button:hover {
				background: var(--accent-color);
				color: white;
			}
		

			@property --angle {
				syntax: "<angle>";
				inherits: true;
				initial-value: 0deg;
			}
			@keyframes angle-spin {
				to { --angle: 1turn; }
			}
			.container {
				animation: angle-spin 4s linear infinite;
			}
		
However, if you look more closely, the CSS syntax has several advantages. CSS variables are actual CSS properties, and share their reactive nature. We can animate them, set them with JS, change them with media queries, and everything automatically adapts.

Sass Calculations


				$spacing: 16px;

				button {
					padding: $spacing / 2;
				}
			

				button {
					padding: 8px;
				}
			

Sass Calculations


				$spacing: 16px;

				textarea {
					padding: $spacing / 2;
					width: 100% - $spacing;
				}
			

				Incompatible units: 'px' and '%'.
			

Native CSS Calculations

calc() in 2013


		

				:root {
					--spacing: 16px;
				}

				textarea {
					padding: calc(var(--spacing) / 2);
					width: calc(100% - var(--spacing));
				}
			

Sass Color Calculations


			$color-accent: #037465;

			button {
				background: lighten($color-accent, 20%);
			}
		

			button {
				background: #06d7bc;
			}
		

But…


				$color-accent: oklch(.5 .3 180);

				button {
					background: lighten($color-accent, 20%);
				}
			

				argument `$color` of `lighten($color, $amount)` must be a color
			

CSS Relative Colors

Coming soon!


				button {
					background: oklch(from var(--color-accent) calc(l * 1.2) c h);
				}
			

				button {
					background: oklch(from var(--color-accent) calc(l + .2) c h);
				}
			
You may be starting to see a pattern: Once more the native CSS version involves clunkier syntax but is more powerful.
Preprocessors CSS
Performance requirements Looser Stricter
Syntax restrictions Looser Stricter
Iteration speed Faster Slower
Context Awareness None Full
Reactivity None Yes
Implementation Limits CSS C++
Preprocessors and native CSS syntax have different strengths and weaknesses. - Native CSS syntax is subject to much stricter performance requirements. Preprocessors only run once, split out CSS, and are done with it, so anything that runs in speed acceptable for a build tool is fair game. CSS doesn't have that luxury. CSS code can be changed and need to re-run millions of times per second, so it has to be blazing fast. This means that certain things that are simple for preprocessors are very hard to do in CSS. - Native CSS syntax is subject to much stricter syntax restrictions. Any syntax added to CSS has to be *both* backwards compatible with existing CSS as well as forwards compatible with any possible future CSS. While this is also a concern for preprocessors, things are a bit easier there due to versioning. - Preprocessors have much faster iteration speed. Adding a new feature requires consensus among the preprocessor maintainers, then it can be implemented and shipped soon after. Adding a new feature to CSS requires consensus among the CSS WG, then someone to write the specification, iterate on it, ensure the spec is implementable, convince browser vendors to implement it, then wait for it to be implemented, then wait for it to be shipped, then wait for it to be widely adopted. The length of this process varies, but it's usually measured in years. - However, native CSS syntax has full context awareness. It knows what the viewport size is, which media queries apply, what is supported and what isn't, what are the metrics of the font being used, etc. This allows native CSS syntax to be a lot more powerful. - Native CSS syntax is reactive. If any of its components change, it will update automatically. Preprocessor syntax is executed top to bottom, analogously to imperative programming. - Lastly, because preprocessor syntax has to be ultimately compiled to CSS, it is subject to the same implementation limits as CSS. However, native CSS syntax can *extend* the bounds of what is possible in CSS, and is only bound by the implementation limits of the browser’s C++. This is not a competition: Preprocessors are excellent for prototyping CSS's future with a tighter feedback loop, and allow us to get feedback from real developers, using these features on real projects.

Nesting

Nesting 101


			table.browser-support {
				border-collapse: collapse;

				th, td { border: 1px solid silver; }
				th { border: 0; }

				td {
					background: yellowgreen;

					&:empty { background: red; }
					> a { color: inherit; }
				}
			}
		

			table.browser-support {
				border-collapse: collapse; }
			table.browser-support th,
			table.browser-support td {
				border: 1px solid silver; }
			table.browser-support th {
				border: 0; }
			table.browser-support td {
				background: yellowgreen; }
			table.browser-support td:empty {
				background: red; }
			table.browser-support td > a {
				color: inherit; }
		
- Nesting was designed to avoid the duplication and error-prone-ness of overly long selectors with a shared prefix. - By default selectors nested inside each other are assumed to be descendants. - To override that, authors can use the `&` character to refer to the parent selector.

Concatenation 🚫


			.card {
				display: grid;

				&__image {
					border-radius: 50%;
				}

				&__button {
					&--submit { background: var(--accent1); }
					&--canel { background: none; }
				}
			}
		

			.card {
				display: grid;
			}
			.card__image {
				border-radius: 50%;
			}
			.card__button--submit {
				background: var(--accent1);
			}
			.card__button--canel {
				background: none;
			}
		
Sass also did concatenation, to facilitate patterns like BEM. Because preprocessors are text-based, things like this are easier. This has always been out of scope for CSS and that remained constant until today, so we will not discuss it more going forwards. In fact, this feature was recognized as [a mistake by Sass’ designers](https://github.com/w3c/csswg-drafts/issues/2701#issuecomment-395572904) too.

Nesting @-rules


			h1.make-it-pop {
				color: var(--brand-color);
				@supports (-webkit-background-clip: text) {
					background: var(--brand-gradient);
					-webkit-background-clip: text;
					color: transparent;
				}

				font-size: 250%;
				@media (width < 500px) {
					font-size: 150%;
				}
			}
		

			h1.make-it-pop {
				color: var(--brand-color);
				font-size: 250%;
			}
			@supports (-webkit-background-clip: text) {
				h1.make-it-pop {
					background: var(--brand-gradient);
					-webkit-background-clip: text;
					color: transparent; }
			}
			@media (width < 500px) {
				h1.make-it-pop { font-size: 150%; }
			}
		

Contextual styling


			.callout.info {
				color: var(--color-blue-dark);
				background: var(--color-blue-light);

				.dark & { /* same as &:is(.dark *) */
					color: var(--color-blue-light);
					background: var(--color-blue-dark);
				}
			}
		

			.callout.info {
				color: var(--color-blue-dark);
				background: var(--color-blue-light);
			}

			.dark .callout.info {
				color: var(--color-blue-light);
				background: var(--color-blue-dark);
			}
		
While rare, the ampersand can come later in the selector as well, usually to create variations depending on the context

Can we adopt this in CSS?

So we define the Sass syntax minus concatenation as the Holy Grail Syntax. However, there are some challenges in supporting this natively in CSS without modifications. To understand these, we need to talk a little bit about how CSS parsing work. It will be short, I promise.

CSS Parsing Crash Course

Parsing

CSS tokens

<number-token>
<hash-token>
- Parsing always begins with tokenization. - Tokens are the smallest meaningful units in a programming language - Tokens are to programming what words are to natural language. - Here are some examples of tokens in CSS, shown as railroad diagrams directly from the CSS Syntax spec.

CSS Tokenization Example


			#nav > a:hover {
				background: white url("bg.png");
				text-decoration-thickness: .1em;
			}
		
<hash-token> <whitespace-token> Single codepoint tokens (<delim-token>, <colon-token>, etc) <ident-token> <function-token> <string-token> <dimension-token>
- The first step of parsing CSS (and any code really) is tokenization. - The tokenizer reads the code character by character and groups them into tokens, i.e. meaningful syntactical units. - For CSS, these tokens are defined in the [CSS Syntax](https://drafts.csswg.org/css-syntax/) specification.

CSS Grammars

Simple selectors (simplified)

[ ]
Grouping
|
Alternative
?
Optional
*
0 or more
+
1 or more
#
1+, comma-separated
- The next step is parsing, which is the process of grouping tokens into constructs. - How this happens is defined in a compact form called a grammar. - [`<any-value>`](https://drafts.csswg.org/css-syntax-3/#typedef-any-value) is a special token defined to allow any stream of tokens except open structures (unmatched parentheses, brackets, braces, broken strings, etc). - This is a simplified version. [Full selector grammmar](https://www.w3.org/TR/selectors/#grammar) - [Full declaration grammar](https://www.w3.org/TR/css-syntax-3/#grammar)

CSS Grammars

Simple selectors (simplified)

- The next step is parsing, which is the process of grouping tokens into constructs. - How this happens is defined in a compact form called a grammar. - [`<any-value>`](https://drafts.csswg.org/css-syntax-3/#typedef-any-value) is a special token defined to allow any stream of tokens except open structures (unmatched parentheses, brackets, braces, broken strings, etc). - This is a simplified version. [Full selector grammmar](https://www.w3.org/TR/selectors/#grammar) - [Full declaration grammar](https://www.w3.org/TR/css-syntax-3/#grammar)
- Start - `<ident-token>` - `<type-selector>` - `<hash-token>` - `<id-selector>` - `'.'` - `<class-selector>` - `'['` - `<attribute-selector>` - `':'` - `<ident-token>` - `<pseudo-class-selector>` - `':'` - `<pseudo-element-selector>`
- Note that for most of these, we can tell what we have from the very first token. Only to distinguish pseudo-classes from pseudo-elements do we actually need two tokens, i.e. we need to "look ahead". - This is not a coincidence, it is by design. - Being able to determine the construct from the current token and by looking ahead at a fixed number of tokens is called "fixed lookahead" or LL(k) and is a very desirable property in parsers. - The CSS parser specifically is LL(1), i.e. needs to only look ahead at most one token.

CSS Grammars

Declaration (simplified)

- [`<declaration-value>`](https://drafts.csswg.org/css-syntax-3/#typedef-declaration-value)

What could this be?

article:is(h1, h2, h3, h4, h5, h6)
background:conic-gradient(red, yellow, lime, aqua, blue, magenta)
- Take a look at this token stream here. Can you guess what CSS construct it represents? - Could it be a selector, like this one? Or maybe a declaration, like this one? - Let’s try to tokenize them and find out!
Without nesting we wouldn’t even try both rules at once, we would know whether we’re parsing a rule or a declaration in the first place, so there is no ambiguity. But with nesting, every time we’re parsing the inside of a rule, we could have either. Would that make CSS ambiguous? Would it make it like the elephant in the well known parable who appeared to be different things to different blindfolded people depending on which part they touched?

Resolving ambiguity

article:is(h1, h2, h3, h4, h5, h6) {
background:conic-gradient(red, yellow, lime, aqua, blue, magenta);
Nah, it’s not as bad as that. Eventually, we would read an opening brace (for a selector) or a semicolon or closing brace (for a declaration) and we'd know what we have. But do note that we lost that very desirable fixed lookahead property: we now need to read a potentially unbounded number of tokens to know what we have. So when we started exploring the possibility of adding Nesting to CSS natively, this was a non starter. No browser would implement a CSS feature that would so fundamentally change CSS parsing. Remember, CSS needs to be blazing fast, and backtracking is slow.

First nesting proposal

[Read a copy of the proposal](./tab-specs/v0/css-nesting/Overview.html)

Syntax

- `{ }` around nested rules - Optional `&` - No @-rule nesting
```css table.browser-support { border-collapse: collapse; { th, td { border: 1px solid silver; } th { border: 0; } td { background: yellowgreen; { &:empty { background: red; } > a { color: inherit; } } } } } ```

PostCSS implements

New syntax

- Dropped `{ }` - Mandatory `&` or `@nest` prefix - Still no @-rule nesting
[Read a copy of the proposal](./tab-specs/v1/css-nesting/)

Changes


			.callout.info {
				color: var(--color-blue-dark);
				background: var(--color-blue-light);

				{
					.dark & {
						color: var(--color-blue-light);
						background: var(--color-blue-dark);
					}

					a { color: var(--color-blue); }
				}
			}
		

			.callout.info {
				color: var(--color-blue-dark);
				background: var(--color-blue-light);

				@nest .dark & {
					color: var(--color-blue-light);
					background: var(--color-blue-dark);
				}

				& a { color: var(--color-blue); }
			}
		

PostCSS updates

Interestingly, the plugin supported @-rule nesting, even though the spec did not.
Well, not quite 84 but it sure felt like it. Part of the reason not much was happening was that a few people thought nesting was better served by a Houdini custom @-rule.

Ping #1

Ping #2

Ping #3

CSS WG adopts spec

Years pass…

State of CSS Survey

Chrome suddenly very keen to implement

Lea, we cannot let this ship!
This will be used all over stylesheets. Having to prefix every rule with `&` or `@nest` will be tedious to write, easy to forget, and noisy to read.
And it’s incompatible with `@scope`, so copying and pasting will require a ton of fixup.
We must do something, ASAP!

Elika Etemad

Extremely prolific CSS editor

(In actuality we had a video call, and this is my recollection of the conversation)

@scope

Proximity scope


				.light a { color: darkmagenta; }
				.dark a { color: plum; }
			

				@scope (.light) {
					a { color: darkmagenta; }
				}

				@scope (.dark) {
					a { color: plum; }
				}
			
- Remember when you were first learning CSS? I don’t know about you, but I was very surprised to learn that proximity did not play a role in specificity at all. - Take a look at this markup. What color will the links be? Based on this CSS, they will both be `plum`. If we flip the order of rules, they will both be `darkmagenta`. Is this intuitive? - `@scope` solves this age old problem by making proximity part of the cascade. - In addition, it also offers “donut scope”, i.e. being able to exclude entire subtrees. - Just shipped in Chrome

Further breakouts

Lea Verou

That’s me!

Tab Atkins-Bittner

Editor of CSS Nesting

Miriam Suzanne

Sass core contributor

Jen Simmons

Developer Advocate @ Apple

Brad Kemper

Web Designer/Developer

Shortly after, there were two seminal breakouts among these people, all seasoned CSS WG members and co-editors of several specs. We had general consensus that the syntax at the time needed improvement before it shipped in browsers, and several ideas about how to improve it.

We share with the WG

[Read summary](https://github.com/w3c/csswg-drafts/issues/7834#issuecomment-1268957000)

Easy win & allowed everywhere

- Just this single-handedly improved compatibility with `@scope` tenfold. - Even with no changes to the nesting syntax, now only selectors with `@nest` would need to be edited when moving code from a nested block to an `@scope` block. Still a lot of compat problems the other way though.
A period of heavy activity followed. There were dozens of proposals, that were very hotly debated. In these types of discussions, the big barriers to reaching consensus are not only people assigning different weights to the various pros and cons, but also people disagreeing about what even is a pro or a con. For example, while most people thought that syntaxes allowing `&` to be implicit in descendants was a pro, for many others it was a con, because they thought it should always be mandatory because it added clarity.

Strategies to resolve ambiguity

  1. Per-rule prefix
  2. Nest rules in block
  3. Parser switch

1 Per-rule prefix

Selectors of nested rules start with a disambiguating token


			ul {
				list-style: "🦄";

				& li { margin: 0; }
				& a { color: inherit; }
				@nest nav & {
					list-style: none;
				}
			}
		

			ul {
				list-style: "🦄";

				@ li { margin: 0; }
				@ a { color: inherit; }
				@ nav & {
					list-style: none;
				}
			}
		
  • Backwards compat with postcss-nesting code
  • Allows mixing rules and declarations
  • Context-free interpretation
  • Tedious: O(N) extra syntax on the number of rules
  • Easy to forget prefix
  • Noisy to read
  • Incompatible with @scope

2 Nest rules in block

Nested rules are wrapped in a `{…}` block or @rule


			ul {
				list-style: "🦄";

				{
					li { margin: 0; }
					a { color: inherit; }
					nav & {
						list-style: none;
					}
				}
			}
		

			ul {
				list-style: "🦄";
			}
			{
				li { margin: 0; }
				a { color: inherit; }
				nav & {
					list-style: none;
				}
			}
		

			ul {
				list-style: "🦄";

				@nest {
					li { margin: 0; }
					a { color: inherit; }
					nav & {
						list-style: none;
					}
				}
			}
		
- This can be further broken down into two subcategories: block nested within the parent rule, or block that *follows* the parent rule (post-nesting) - There were many more ideas about what should come before the opening brace, even ASCII like `&&` or `@`, but in the end the two main options were "nothing" and `@nest`.
  • Unrestricted selector syntax
  • O(1) extra syntax on the number of rules
  • No mixing of declarations and rules
  • Extra indentation level (for nested versions)
  • No clear path to inline style nesting (for post-nesting)

3 Parser switch

A specific token that signifies "only rules from now on"


			ul {
				list-style: "🦄";

				@nest;

				li { margin: 0; }
				a { color: inherit; }
				nav & {
					list-style: none;
				}
			}
		

			ul {
				list-style: "🦄";

				@media (width < 500px) {
					margin-left: 1ch;
				}
				li { margin: 0; }
				a { color: inherit; }
				nav & {
					list-style: none;
				}
			}
		

			ul {
				list-style: "🦄";

				& li { margin: 0; }
				a { color: inherit; }
				nav & {
					list-style: none;
				}
			}
		
In this strategy, as long as we encounter a non-declaration, we assume that everything after is a rule. This could be a token specifically for this, such as `@nest`, or any other known rule, including a no-op `@nest`.
Rule Prefix Parser switch Block inside Block after “Lea’s proposal”
No syntax overhead per nested rule 🚫 🚫
Allows mixing rules and declarations? 🚫 🚫 🚫
Reordering not error-prone 🚫
Compatible with @scope? 🚫
Same indentation level? 🚫
Compatible with Holy Grail Syntax? 🚫 🚫 🚫
Clear path for style attribute 🚫

Should we rule out rule prefix syntaxes?

“Lea’s proposal”

  • Any non-ident starts a nested rule
  • Any nested rule must start with a non-ident
At some point I posted a new idea: what if we went with a prefix-based syntax, but the prefix could be anything other than an ident? This came to be known as “Lea’s proposal” so for the lulz I’ll use that name for now as well.

				ul {
					list-style: "🦄";

					.foo { }
					li { }
					> a { }
					.dark & { }
					#sidebar & { }
					nav & { }
				}
			

Holy Grail Syntax


				ul {
					list-style: "🦄";

					.foo {  }
					& li { }
					> a { }
					.dark & { }
					#sidebar & { }
					:is(nav) & { }
				}
			

“Lea’s Proposal”

- `&` is optional. If no `&` is present, the rule is interpreted as a descendant - If a descendant rule starts with a type selector, then the `&` is mandatory - Drop `@nest` to maximize compatibility. Contextual selectors that need to start with a type selector must be rewritten. - It combined a lot of the advantages of prefix-based syntaxes (context independence, mixing rules and declarations), without the tediousness of having to add an additional prefix to every single selector. - Dropping `@nest` ensures full compatibility with `@scope`, and offered a clear upgrade path to the Holy Grail Syntax. - Proposal was generally very well received - Some concern that the distinction about when `&` is not optional would be confusing.

What do you want the general rule to be?
592 respondents

Mandatory &
Optional &
(for descendants and combinators)

What do you want to do in your own code?
1,613 respondents

Explicit `&`
Omit `&` whenever possible
*(Percentages normalized to exclude "Just show results" answers)* **[Poll 1: What do authors prefer for their own code](https://twitter.com/LeaVerou/status/1579902585540345857)** (1,613 votes) Which of the following best expresses how you want to write nested CSS when it's implemented by browsers? - (**52%**) I want to use `&` in every selector, as I think it makes code more clear - (**48%**) I want to be able to omit `&` whenever possible, as it's noisy to read and a hassle to type **[Poll 2: What do authors want to be the general rule](https://twitter.com/LeaVerou/status/1580215877705687040)** (592 votes) If it were up to you, what syntax would you prefer for CSS Nesting? - (**42%**) `&` should be mandatory, even in descendants and combinators - (**58%**) `&` should be optional for descendants and combinators

Use data to short-circuit long debates
Even very imperfect data works better than arguing

- The more statistics-minded among you may be horrified at the thought of using Twitter polls as a source of data. “But the snowballing bias!” I can hear you crying. - Yes, Twitter polls are not perfect, but nothing gets you the same amount of data in the same amount of time, and with their flaws and all, they’re still better than pure speculation. - In general, data works wonders in short-circuiting discussions that do not seem to be converging towards consensus. - Don’t worry about the data being imperfect. For reaching consensus, it's more important to get data fast, than to minimize all possible sources of bias. Getting good data will be no help if it arrives long after the discussion has ended. It’s like prototyping: iterating fast is more important than getting it right the first time.

Finalists

  1. (Then) current spec
  2. Parser switch
    1. Any @rule
    2. Any @rule + `&`
    3. Any non-ident
  3. “Lea’s proposal”
  4. Post-nesting proposal
    1. `@nest {}`
    2. `& {}`
    3. `{}`

WG consensus to switch to Option 3

While I initially supported option 1, having looked through the details, option 3 is very good
It most of the time gives you the ability to write like in Sass, with one awkward exception that’s easy to tell apart
The rule for whether you need [an ampersand] or not is very clear, and this does allow us to be close to Sass
- [Minutes](https://github.com/w3c/csswg-drafts/issues/7834#issuecomment-1284314893) - Note that the resolution was that we prefer Option 3 over the current spec (at the time), but Option 3 vs Option 4 would be discussed later.

🚀 Spec updated 🚀 Chrome implementation updated

- [Spec updated](https://github.com/w3c/csswg-drafts/issues/7834#issuecomment-1293948163) - [Chrome changes in review](https://github.com/w3c/csswg-drafts/issues/7834#issuecomment-1294064507)

2022 was not the first time I proposed that idea 🤫

If your idea is rejected, try again later? 😀
(data helps too)

WebKit polls developers once more

Option 3 wins by a landslide

Final WG resolution to adopt Option 3

[Minutes](https://lists.w3.org/Archives/Public/www-style/2022Dec/0015.html)

So… are we done now?

Core idea Allow backtracking, as an edge case

- What if we could minimize the number of cases that require backtracking so much it becomes exceedingly rare? - “Asymptotically fixed lookahead” 😅

Observation 1
Far more declarations than nested rules

Quite trivially for most existing stylesheets, but this holds true even for stylesheets that use nesting quite heavily.

Observation 2
Very few nested selectors are actually ambiguous (< 2%)

For simplicity, option 3 requires rules to not start with an ident. However, not everything that starts with an ident is actually ambiguous.

Parse as declarations, backtrack if no luck

- 0 perf impact for declarations, and tiny for 2% of rules

Implementer Feedback

This may work; needs measurements to assess impact

Emilio A.

Mozilla

I want to get any syntax specified and be done with it.
So NAK. I won’t be spending resources on this. Non-starter for us.

Steinar G.

Google

If Chrome has trouble with this, we will too 🤷🏼

(Redacted)

Apple

Formal Objection if feasibility of the Holy Grail Syntax is not investigated for reals

Peter Linss

W3C TAG co-chair

Tab, Peter, and others refine algorithm further

Holy Grail Syntax actually possible in Chrome! 🎉

Take “Not implementable” with a grain of salt

It’s common for things that originally seemed not implementable to prove out to be implementable upon closer inspection. This is by far not the only example. Container queries, `:has()` are two big others.

100% seems a lot easier once you’re at 80%

The fact that we were almost there definitely played a role in people being motivated to get all the way there. And getting to 80% requires less than 80% of the effort.

Favor internal complexity over external complexity

- Getting to the syntax that is simpler on the outside meant that parsing is now significantly more complex. But usually offloading complexity inwards is a worthy tradeoff. - Each interface is used by far more people than the number of people who have to deal with the internals, and the internals can always be improved down the line. - In fact, the opposite is a usaiblity antipattern: degrading the user experience to make implementation easier.

Current state

Surely we’re done now?!

Still lots to do!

- Nesting in `style=""` - CSS OM - [Cascade of declarations after rules](https://webkit.org/blog/14571/css-nesting-and-the-cascade/) - [and a lot more…](https://github.com/w3c/csswg-drafts/issues?q=is%3Aopen+label%3Acss-nesting-1+sort%3Aupdated-desc)