csswg-drafts icon indicating copy to clipboard operation
csswg-drafts copied to clipboard

[css-nesting] Syntax suggestion

Open proimage opened this issue 4 years ago • 96 comments

This is kind of a follow-on from the closed issue #2701 and #2937.

Would it make any sense to implement native CSS nesting something like this?

body {
    background: black;
    color: white;
    ( /* note the open parentheses to indicate everything within is nested */
        main {
            background: orange;
            color: black;
        }
        p {
            font: serif;
        }
    ) /* close parentheses */
}

Or perhaps even like this (uses existing parser patterns):

body {
    background: black;
    color: white;
    @nest { /* or @nested, or @child, etc */
        main {
            background: orange;
            color: black;
        }
        p {
            font: serif;
        }
    } /* END NEST */
}

Basically, it would require any nesting to be placed within a separate grouping container, as a way to differentiate in bulk between attributes and nested selectors. Seems to me that this would have the benefit of not using the ampersand character and thus avoiding complications with pre-processors...

proimage avatar Feb 05 '20 19:02 proimage

The drawback is that it adds an additional level of indentation.

The mandatory & makes intention of a developer a bit more obvious and clear. I see no any good reasons of trying to avoid it. I would even make the @nest prefix mandatory for all nested rules to make it super explicit, but current spec is also fine.

vrubleg avatar Nov 12 '20 20:11 vrubleg

So the proposal is that a bare @nest with no selector creates a rule that cannot contain any declarations but only CSS rules, which are all assumed to be nest-containing?

I wouldn't be opposed to that. It would save quite a few characters in nested rules with high breadth and low depth, and would make them easier to read too without the repetitive @nest.

Do note however that @tabatkins has objected to defaulting to descendant selectors when no & is present, and his arguments are quite sound.

LeaVerou avatar Dec 16 '20 16:12 LeaVerou

I'll be completely honest here—I don't fully understand everything going on here. ¯\_(ツ)_/¯ I build with CSS; I don't build CSS. ;)

So all I can say is that I have a strong gut feeling that implementing native CSS nesting in a way that breaks the compatibility that SCSS currently has with pure CSS is a bad move. Yes, I know the onus is not on CSS to retain that compatibility. Regardless, I feel like intentionally breaking it (or allowing it to break) when there are other ways to implement things will actively disrupt a large portion of the web.

I presume that for whatever reason, implementing native CSS nesting in the exact same way that SCSS already does it has been ruled out?

proimage avatar Dec 16 '20 21:12 proimage

I presume that for whatever reason, implementing native CSS nesting in the exact same way that SCSS already does it has been ruled out?

The Nesting spec goes into detail about why that's not an option.

LeaVerou avatar Dec 17 '20 01:12 LeaVerou

@proimage Current spec is close to perfect, and it doesn't break compatibility with SCSS. SCSS couldn't be used in browser directly ever, you always had to use the SCSS preprocessor. So, just continue to use this preprocessor, and everything will be fine. It is not mandatory to use native CSS nesting when it will be available in browsers. But I wold use it, because it seems that it is designed a bit better than the SCSS nesting.

vrubleg avatar Dec 17 '20 14:12 vrubleg

Current spec is close to perfect, and it doesn't break compatibility with SCSS.

Great! If that's the case, then I have no objection. :)

proimage avatar Dec 22 '20 08:12 proimage

If authors of CSS preprocessors want to allow mixing of the native CSS nesting and SCSS-like nesting in one file, they could rely on @nest. If there is @nest before nested selector, it means that a user wanted to use native CSS nesting.

vrubleg avatar Dec 22 '20 08:12 vrubleg

My main issue with the specified approach is that there are two distinct syntax rules for authors, in order to solve a browser-parsing issue. While I understand the parser requirement in play, I don't like passing that along as an inconsistent syntax, where authors have to understand the parsing distinction. It would be great if we could move towards a more consistent single syntax, no matter what nested selector you plan to write.

In talking with @fantasai, we had a few ideas for a variation on the approach suggested above:

div {
  background: black;
  color: white;

  /* curly braces always fence off nested selectors, so we avoid syntax disparities */
  /* by default & is required in all selectors, to establish relationship */
  {
    & span { /* div span */ }
    p & { /* p div */ }
  }

  /* multiple nesting blocks are allowed */
  /* and are able to prepend a combinator for the entire block */
  /* (or & for descendant) */
  & {
    span { /* div span */ }
    p & { /* div p div */ }
  }

  ~ {
    span { /* div ~ span */ }
    p + & { /* div ~ p + div */ }
  }

  /* could also use & before combinator, for the same results */
  & ~ {
    span { /* div ~ span */ }
    p + & { /* div ~ p + div */ }
  }
}
  • It's a single, concise syntax for any type of nested selector
  • it allows the author to establish meaningful/readable shorthands that avoid repetition
  • the shorthand also helps to group selectors which share a similar relationship to the parent

mirisuzanne avatar Sep 21 '21 15:09 mirisuzanne

Hmm, interesting idea. Similar (and simpler, even) than the suggested @nest { /* selectors */ } option, just without even needing the @nest bit?

So what would multiple-level nesting look like?

nav.main-nav
{
   display: flex;
   {
      ul
      {
         list-style-type: none;
         {
            li
            {
               margin: 0;
               padding: 0;
            }
            a
            {
               display: flex;
               padding: 0.5em 1em;
            }
         }
      }
   }
}

Or, in the inline-braces style y'all seem to prefer for some reason... 😉

nav.main-nav {
   display: flex;
   {
      ul {
         list-style-type: none;
         {
            li {
               margin: 0;
               padding: 0;
            }
            a {
               display: flex;
               padding: 0.5em 1em;
            }
         }
      }
   }
}

All I'll say is, my eyes certainly aren't used to visually parsing that code... granted, color-coding would help a ton, but still... 😆

proimage avatar Sep 22 '21 14:09 proimage

I agree. I know lots of people have worked on this, but I've been following the discussion and I also feel like passing down the burden of having 2 syntaxes to solve a parsing problem to the CSS author is not the best move.

If @nest is able to forgo the requirement of the selector having to start with &, it just means it's a parsing issue that can be solved for the "regular" syntax too. And it's much cleaner to use and read when blocks are nested naturally with curly braces.

I'd say the last example by @proimage would be repetitive though, I would wish the curly braces to be unique per level if it was possible. Right now, CSS parses everything inside braces as a style declaration and that's the difficulty "@nest" and "starting with &" are trying to bypass : it's easier to parse the start of a nesting selector if it starts with something precise. But really if there's another curly braces block inside, it should be reverse parsed... or the rule could be "what precedes a curly brace needs to be a selector" point bar.

Again, same as some others chiming in: I have not worked on the parsers, I don't know the real difficulties of this, but if CSS parsers need to be rewritten to allow this, I would way prefer to wait and do it properly than ship this as it is.

Here's an example of the syntax I'm trying to explain:

.main-nav {
   display: flex;
   ul {
      list-style-type: none;
      li {
         margin: 0;
         padding: 0;
      }
      a {
         display: flex;
         padding: 0.5em 1em;
      }
   }

   nav& {
      display:  block;
   }
}

Becomes

.main-nav { display: flex; }
.main-nav ul { list-style-type: none; }
.main-nav ul li { margin: 0; padding: 0; }
.main-nav ul a { display: flex; padding: 0.5em 1em; }
nav.main-nav { display:block; }

davidwebca avatar Sep 22 '21 16:09 davidwebca

@davidwebca Sadly, it's just not possible to "reverse parse" anything in CSS. For performance reasons, browser engines need to distinguish between a property and a nested selector with only a single token.

mirisuzanne avatar Sep 22 '21 17:09 mirisuzanne

Here's an example of the syntax I'm trying to explain: * snip *

As was posted when I raised the very same issue, the answer is given here (in the expandable green details+summary box): https://drafts.csswg.org/css-nesting/#nesting

You're not wrong in that it would be an ideal syntax from a code authoring perspective, but from the perspective of the parser, it would exact too high a performance toll.

EDIT: Dangit, comment-sniped! ;)

proimage avatar Sep 22 '21 17:09 proimage

I love this idea. I agree having two syntaxes depending on the location of the & is suboptimal. This also means we can have the simple descendant syntax too, without having to prepend with & , making migration from preprocessor stylesheets as simple as wrapping rules with a set of {}.

LeaVerou avatar Sep 24 '21 15:09 LeaVerou

So, put simply, if, within rule braces, the parser encounters another opening brace, combinator, or ampersand (which isn't part of the value for a declaration), it should then go into selector parsing mode, instead of declaration parsing?

Interesting. I guess any preceding declaration values need to be capped off with a semicolon, to avoid a comma as a combinator being confused with being part of a value.

bradkemper avatar Sep 26 '21 04:09 bradkemper

So, put simply, if, within rule braces, the parser encounters another opening brace, combinator, or ampersand (which isn't part of the value for a declaration), it should then go into selector parsing mode, instead of declaration parsing?

Interesting. I guess any preceding declaration values need to be capped off with a semicolon, to avoid a comma as a combinator being confused with being part of a value.

I think what we're talking about here is dropping the ampersand entirely (unless the parent selector needs to be injected in the nested selector in some other position) in favor of curly braces wrapping any nested selectors. Keeps the syntax simpler—no need for both & and @nest & support.

proimage avatar Sep 26 '21 09:09 proimage

Would this open the door for selector concatenation via the &?

.card {
    display: flex;
    {
        &__image {
            display: block;
        }
    }
}

proimage avatar Sep 26 '21 09:09 proimage

Dropping & removes the ability to combine and reverse order of selectors so I wouldn't drop it. I think what we're suggesting here is to merely drop the requirement of the selector having to start with & (or to use @nest to allow it). Right now, the draft spec requires the selectors to start with & to pass the nesting requirement. Here's an example that is invalid:

.card {
    display: flex;
    .image& {
        display: block;
    }
}

To make it valid, you need to prefix it with "@nest" like so:

.card {
    display: flex;
    @nest .image& {
        display: block;
    }
}

So what we're saying, in short, is to drop the @nest requirement and stop dancing around the current parsers limitation to allow authors to have a cleaner syntax without those two versions that could end up being confusing and allow the first example to be valid (if it works with @nest, why wouldn't it be able to work without?)

Sidenote, I personally don't care about selector concatenation. That, I can understand it adds a level of complexity that jumps into many more parsing hoops and issues and I don't think people absolutely really need it. BEM authors can use BEM with combinations instead of concatenations (.card__image would be .card.__image) and still be readable. If it was possible without being too complex and without adding too much parsing time to browsers, I would like it, but maybe we're not there yet.

davidwebca avatar Sep 27 '21 14:09 davidwebca

I'm really not a fan of the additional set of braces; it adds two indents for each level of nesting.

I'm not sure what your additional three syntaxes are doing - is a bare selector allowed (not nested in an extra {}) if it starts with an &? If so, then I'm not sure what this gets us over putting @nest in front of everything - you'd still have two syntaxes and have to know when to switch from one to the other. You get to avoid writing a @nest keyword in front of each of your selectors, but in return you have to indent an extra level each time and deal with more matched braces.

Is the ~ one doing some implicit relative combinators? That's even more powerful than what Sass/etc currently allow, and means there's more context for a reader to carry into parsing the nested selectors - seeing a span {...} doesn't mean "a span that's a descendant of the parent rule's element" like it does in Sass, but instead can mean it has any of the four combinator relationships with the parent.

Actually, hm, it looks like you still can't put properties directly in the block; the nested block must contain style rules, not declaration lists, right? Then yeah, I'm still on the "two indents per nesting level sounds bad" train.

Finally, the current rules allow us to avoid having to write a heuristic for determining when a selector is meant to implicitly chain from the parent and when it's explicitly referencing the parent instead. These appear to bring that heuristic back, so we have to decide, for example, whether a & nested inside of an :is() or :not() counts as "referencing the parent" and so whether it means there's still a & implicitly prepended or not. This heuristic smells like an editing hazard to me, which is why I was so glad to be able to get rid of it with the current proposal, where all selectors are "complete" as written.

tabatkins avatar Sep 27 '21 23:09 tabatkins

The double curly braces from @proimage were not necessary as detailed in my last comment. Is that what you're referring to?

I think the idea here is to mainly combine the regular syntax in the proposal with the @nest rule so that a nested selector is not obligated to start with & and avoid having 2 different nested selector syntaxes. 🤔

davidwebca avatar Sep 28 '21 02:09 davidwebca

I'm really not a fan of the additional set of braces; it adds two indents for each level of nesting.

I'm not the biggest fan of it either, but I think it's preferable over the current & … / @nest … & … proposal. Heck, perhaps in time our eyes would come to appreciate the extra level of indentation as an easy way to visually differentiate nested styles?

That said, what if we used some other separator character or series of characters to define a nesting block—one that doesn't have an implication of indentation?

I can't think of any single char that would fit the bill, but what about something like this?

div {
    color: red;
    ==== /* or ---- or &&&& or whatever */
    p {
        color: blue;
    }
    blockquote {
        zoom: 420;
    }
    ====
}

proimage avatar Sep 28 '21 07:09 proimage

I don't think it's hard to remove any heuristics from our proposal, and require explicit & wherever it's desired. That still leaves the double-indentation, but that doesn't bother me as much as a double syntax.

There would be another approach to achieving single-syntax, which is just to require @nest or some even-more-brief prefix on all nested selectors. Something like (using plain @ for an extremely terse example):

div {
  prop: value;

  @ & em { … }
  @ main & { … }
  @ & ~ div, p + & { … }
}

Though double-nesting still feels the easiest to read and write in my opinion.

mirisuzanne avatar Sep 28 '21 19:09 mirisuzanne

I agree. The double syntax bothers me more than anything else that we've been discussing. Whatever the decision in the end, I'd rather have a single syntax with more indentation OR @nest so that avoids all confusion when reading CSS code at a glance.

Then, I don't mind more indentation and I don't hate the "@" suggestion from @mirisuzanne above. My only gripe with @ is that it's already used to start special keywords like @media and @keyframes. What about ">"? It must have been suggested before, has it? Or would it be too confusing with the direct child combinator?

div {
    color: blue;
    > table td & {
        color: orange;
    }
} 
div {
    color: blue;
    ? table td & {
        color: orange;
    }
} 
div {
    color: blue;
    - table td & {
        color: orange;
    }
} 

davidwebca avatar Sep 28 '21 20:09 davidwebca

ASCII soup isn't great if we can avoid it; anything we choose here becomes probably unusable in Selectors in the future, too.

If the group thought it was really worthwhile to have only a single form, using only @nest or even a bare @ is acceptable to me. I just fear it'll be too annoying for people already used to nesting in preprocessors where nothing is needed at all.

tabatkins avatar Sep 28 '21 20:09 tabatkins

If we're wanting to annoy preprocessor users as little as possible, then I think the "wrap nested selectors in a block" approach would be easier to transition to than the "append @nest/@ and/or & to each individual selector" approach.

If we go down that route, it would be great if the chars used to define said block were one of the ones already considered "containing chars" by code editors... i.e., the chars or char pairs that can easily be placed around a selection: ' ', " ", ( ), [ ], or { }.

To be honest, I'm still trying to figure out if a JSON-esque approach makes any sense:

div {
    color: red;
    nested: (
        img {
            display: block;
        }
    );
}

proimage avatar Sep 29 '21 09:09 proimage

Mmm that's an interesting idea that I didn't consider honestly. That's one thought I was bouncing around in my head when we were exchanging in the previous few comments: "@" rules usually have a set of specific rules (@keyframes, @media, etc.) and allow for specific functionalities, but nesting is just... allowing selectors inside a block , so why would we need an "@" rule when it's just about styles?

In that sense, the nested "style" declaration from @proimage makes syntactical sense. Plus, usually, at-rules only use parenthesis when there's a need to use special characters such as colons inside the arguments. Ex.: @supports (display:flex), @media screen and (min-width:280px)

So an interesting idea that would be easy on existing parsers would look like:

.card {
    display: block;
    (article&) {
        display: flex;
    }
}

would yield

.card { display: block; }
article.card { display: flex; }

But this conversation is running in circles because we're trying to solve three friction points at once when it might not be possible, unless we speak with people who code and optimize those parsers:

  1. Combine the two syntaxes (starting with & and @nest) into one to avoid confusion
  2. Prevent superfluous pressure on parsers by starting nested rules with a special character, ideally one that is not & to avoid confusion with its previous selector reference purpose.
  3. Try and reuse existing pre-processor syntaxes to facilitate migration

Also I want to clarify something about my previous comments on "reverse parsing". I didn't mean "let the parser do it's thing, parse and reverse engineer the selectors afterward". What I meant was to start parsing by the deepest level of nested blocks because it's easy to infer that the previous "rule" is a selector (what precedes "{" is always a selector or an "@" rule). It might be what they already do internally and I wouldn't know about it 🤷 but in that case, it would mean we don't need a starting character at all.

Note: Arguments made by @tabatkins here are still very much valid, but I'd love to hear them in a renewed way one year later.

davidwebca avatar Sep 29 '21 16:09 davidwebca

Just brainstorming here: What if instead of the curly braces, we prepend the list of nested rules with something? E.g. @nested;. That would solve the problem of the extra nesting level.

Alternate idea: What if only the first rule needs to start with &, then we can just assume the rest are selectors and not properties. Then, if anyone wants to handle this generically (e.g. when migrating from a preprocessor), only 3 characters need to be prepended to the list of nested rules: &{}.

LeaVerou avatar Sep 29 '21 19:09 LeaVerou

You get to avoid writing a @nest keyword in front of each of your selectors, but in return you have to indent an extra level each time and deal with more matched braces.

@tabatkins That seems like a really good trade-off for not writing @nest in front of every selector, which is both a) annoying to type and b) uselessly noisy.

I don't understand the resistance to another level of indentation. If your indents are too long, stop using 4 spaces. If you're not mixing in declarations you can also just double up your braces and indent one level.

outer {{
  inner { something: foo; }
  more { other: foo; }
}}

@LeaVerou I don't think I like that kind of statefulness, where what's parsed before as a sibling construct affects so fundamentally what's parsed after.

fantasai avatar Oct 01 '21 18:10 fantasai

I think it's reasonable to be annoyed at needing two levels of indents per nesting level, as you'll accumulate a huge indent very quickly with just a few levels of nesting. "Just use a smaller indent" means that the rest of your code (CSS, JS, HTML), which needs a single indent per nesting level in whatever context it's using, is now not indented enough.

Any efforts to avoid it result in editting hazards. In the example you gave, if the author later does need to add some properties to outer, either they now have to go back and reformat the entire rule contents to put the braces on a separate line and indent things inward another level, or they just put the properties directly in the second level and wonder why their code isn't working.

It ends up that the safest, most reliable way to format the code, which is hardest to make mistakes with and requires the least amount of reformatting when doing adjacent unrelated edits, will be to just wrap each rule in a {} immediately, like:

outer {
	{inner { something: foo; }}
	{more { other: foo; }}
}

This formatting pattern allows the author to add new properties or nested rules to outer with a minimum of fuss or reformatting, and avoids needing two levels of indent per nesting level. The problem is that it's ugly and still somewhat error-prone; while writing the example I forgot to do the double-brace }} on one of the lines, which would have broken the rest of the stylesheet due to misnesting.

In addition to having bad ergonomics on its own, this pattern is completely foreign, looking nothing like common preprocessors or existing CSS, or any other programming language I can come up with immediately.


If we really don't like having two patterns, we can just go with @nest as the only way to nest. It loses the connection with all existing preprocessors' similar feature, but it matches up with standard CSS practice, has only a single level of indent per nesting level, and no excess braces. I don't currently believe that would be a good idea, but I'm open to being convinced, and wouldn't object if the rest of the WG decided that was the way to go.

But I'm absolutely opposed to this extra-braces idea, for the reasons stated above.

tabatkins avatar Oct 01 '21 19:10 tabatkins

I agree. The more I think of it, the less I like the double braces / indent. In the end, we're just trying to avoid having two syntaxes and if they ruled out that it's not possible to write nested selectors without a starting character, I'd rather have @nest be the only one to avoid people getting into weird "but sometimes & works and sometimes it doesn't".

Not opposed to have a "shorthand" version which would be just "@" to start the line, but again there are good sides and bad sides for this.

davidwebca avatar Oct 02 '21 14:10 davidwebca

Ok, another idea: Instead of prepending an instruction that changes parsing, what if the rule is "Always prepend with @nest, but you can mass-prepend by using @nest { ... }"?

I.e. the following would be equivalent:

a.foo {
	@nest &:hover {}
	@nest .bar &:hover {}
}
a.foo {
	@nest {
		&:hover {}
		.bar &:hover {}
	}
}

Then it's not two syntaxes, but a shorthand form of the same syntax, and authors have the option of whether to introduce a second level of indentation or not in their code.

(I'm just brainstorming here, not sure I support the idea)

LeaVerou avatar Oct 04 '21 18:10 LeaVerou