w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.44k stars 657 forks source link

[css-nesting] Syntax suggestion #4748

Closed proimage closed 1 year ago

proimage commented 4 years ago

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...

vrubleg commented 3 years ago

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.

LeaVerou commented 3 years ago

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.

proimage commented 3 years ago

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?

LeaVerou commented 3 years ago

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.

vrubleg commented 3 years ago

@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.

proimage commented 3 years ago

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. :)

vrubleg commented 3 years ago

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.

mirisuzanne commented 2 years ago

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 */ }
  }
}
proimage commented 2 years ago

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... 😆

davidwebca commented 2 years ago

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; }
mirisuzanne commented 2 years ago

@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.

proimage commented 2 years ago

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! ;)

LeaVerou commented 2 years ago

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 {}.

bradkemper commented 2 years ago

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.

proimage commented 2 years ago

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 commented 2 years ago

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

.card {
    display: flex;
    {
        &__image {
            display: block;
        }
    }
}
davidwebca commented 2 years ago

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 (.cardimage 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.

tabatkins commented 2 years ago

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.

davidwebca commented 2 years ago

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. 🤔

proimage commented 2 years ago

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;
    }
    ====
}
mirisuzanne commented 2 years ago

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.

davidwebca commented 2 years ago

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;
    }
} 
tabatkins commented 2 years ago

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.

proimage commented 2 years ago

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;
        }
    );
}
davidwebca commented 2 years ago

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.

LeaVerou commented 2 years ago

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: &{}.

fantasai commented 2 years ago

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.

tabatkins commented 2 years ago

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.

davidwebca commented 2 years ago

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.

LeaVerou commented 2 years ago

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)

davidwebca commented 2 years ago

I like this mainly for the fact that it allows not repeating @nest thousands of times. 🤔

proimage commented 2 years ago

I'd be happy with that solution as well. Just to make sure we cover all bases though, I've got one more syntax proposal that sort of builds on that, but also uses a pattern I haven't seen anyone raise yet. It avoids double-indenting and allows bulk-nesting in one diabolical swoop:

ul.parent {
    padding-left: 0;
}, @nest { // or perhaps [ or (

    // ul.parent li.child
    li.child {
        list-style-type: none;
    }

    // .wysiwyg ul.parent
    .wysiwyg & {
        list-style-type: initial;
    }
}
LeaVerou commented 2 years ago

One problem I ran into using @nest with the current syntax (through a preprocessor) is that it was unclear to me, as an author, how to use @nest with selector lists. E.g. this was my CSS:

&.small,
.small.button-group > & {
    font-size: var(--font-size-x-small);
}

Do I prepend the whole thing with @nest?

@nest &.small,
.small.button-group > & {
    font-size: var(--font-size-x-small);
}

That looks weird though, especially since the @nest is before a selector that isn't supposed to need it. Do I prepend each selector with @nest?

@nest &.small,
@nest .small.button-group > & {
    font-size: var(--font-size-x-small);
}

Nah, surely not. It would look especially weird if the selector was single line.

Maybe I prepend only the complex selector that actually needs it?

&.small,
@nest .small.button-group > & {
    font-size: var(--font-size-x-small);
}

In the end I figured out which one it was, because the others wouldn't compile, and by looking at the spec, but the fact that all of these possible syntaxes looked plausible is a bit of a red flag that the syntax isn't that intuitive. That in itself may argue for ditching @nest and going for one of the enclosing syntaxes proposed.

@tabatkins thoughts?

tabatkins commented 2 years ago

This is kinda a generic complaint about long comma-separated preludes on at-rules, I'd think? We haven't really had them in CSS yet, so we don't have a good indentation strategy established in common culture.

I'd be fine with the "@nest with an empty prelude is allowed, and changes the parsing of its inside to only allow rules, not properties", tho it makes the OM somewhat fiddlier. Hmm.

So if we did that, I think we could handle it as a serialization fix-up: if you try to serialize a @nest rule with a null selector list and properties, we'd just serialize it as having a & selector, and translate child rules into @nest (rather than just being CSSStyleRule).

This does make me wonder slightly if we should dump the CSSNestRule interface entirely, since it's, by design, identical to CSSStyleRule, and just say that nested CSSStyleRules are automatically serialized with @nest (even if we allow, in some situations, them to be written by authors without the at-keyword). To get the null-selector behavior from above we'd have to make .selectorText nullable, which should be fine? (Currently it's specced to throw, tho it appears in Chrome at least we'll happily stringify null and use it as the selector; that's equally broken, tho, so should be all right to change behavior for.)

css-meeting-bot commented 2 years ago

The CSS Working Group just discussed [css-nesting] Syntax suggestion, and agreed to the following:

The full IRC log of that discussion <dholbert> topic: [css-nesting] Syntax suggestion
<astearns> github: https://github.com/w3c/csswg-drafts/issues/4748
<dholbert> TabAtkins: I'll give initial presentation, and then others can argue for their preferences
<dholbert> TabAtkins: leaverou and fantasai have some [...]
<dholbert> TabAtkins: the way it's written now, you can nest a style rule inside of another style rule
<dholbert> TabAtkins: you need to use ampersand
<fantasai> s/[...]/concerns about the currently-proposed syntax, particularly that it has two variants/
<dholbert> TabAtkins: alternately, if you want it somewhere other than at the start, you use the @nest syntax
<dholbert> TabAtkins: two suggestions in the thread. one: always require wrapping set of curly braces, to syntactically distinguish rules from properties
<dholbert> TabAtkins: few other variants on that
<fantasai> see Miriam's comment at https://github.com/w3c/csswg-drafts/issues/4748#issuecomment-924118287
<dholbert> TabAtkins: I objected, don't like it; it means any nested style rule ends up being indented two levels from its parent
<dholbert> TabAtkins: any non-trivial amount of nesting really blows out your margin, gets far over in your code editor
<dholbert> TabAtkins: What I settled on at the end is just to always require an @nest rule. Throw away completely unlabeled rule, always use @nest
<dholbert> TabAtkins: possibly be able to have @nest without a selector and nest style rules inside of that
<dholbert> TabAtkins: not sure I like that part, OM will get a little messy, similar to how layer has two representations for same rule name
<dholbert> TabAtkins: my initial proposal is that you remove the direct nesting and always make you use @nest rule
<dholbert> fantasai: I think that I'd like to pick a syntax that's consistent, so there aren't multiple variations that are different. that's confusing
<dholbert> fantasai: we should pick a syntax that's not noisy, since this is going to be used all over the place. This is a structural thing, and basic; I expect to see this a lot in the future
<dholbert> fantasai: I don't have a problem with the extra indentation; that's an argument for not using very large indents, basically
<dholbert> fantasai: leaverou raised some issues about how you'd handle the nested version of [...] mixed with selector lists, which is awkward
<TabAtkins> Lea's comment: https://github.com/w3c/csswg-drafts/issues/4748#issuecomment-940160175
<dholbert> fantasai: I think the curly-brace syntax is fine, I understand you don't like it, want to hear from other people
<dholbert> astearns: I'm not a big fan of the pure-symbol syntax, whether it's braces or ampersand. more difficult to notice, search for
<dholbert> astearns: if everything required @nest, that seems easier to read, if slightly more difficult to type
<dholbert> fantasai: you have to balance that against how basic is the syntax & how often it will be used
<dholbert> fantasai: e.g. we don't have @style for style rules; we just have selectors
<dholbert> fantasai: we don't write begin/end; CSS syntax is built out of these symbols
<dholbert> fantasai: we have other selectors like @font-face which aren't used all the time, so they have to be clear
<dholbert> fantasai: nesting syntax is closer to the former case, so most stylesheets will have a lot of it. so we don't need to have a keyword to make it searchable; it'll just be an obvious way to structure your CSS
<dholbert> fantasai: people have been using nested selectors in preprocessors without any kind of keyword
<dholbert> fantasai: maybe we end up with a keyword, but I don't know that we need to
<dholbert> fantasai: having it all over your stylesheet, in front of every single rule, would be terribly noisy. they're not important
<dholbert> fantasai: the markup should fade into the background
<dholbert> astearns: is that an argument for only doing open/closing braces?
<dholbert> fantasai: if we're insisting on a keyword, we could do @nest with braces. If we have it in front of every selector, that would be noisy
<dholbert> fantasai: if we're going to have curly braces, you might as well just put them there. don't need a keyword
<dholbert> fantasai: as long as we're not requiring it in front of everything, it's less of an issue
<dholbert> miriam: not surprisingly, I agree with fantasai
<TabAtkins> https://www.irccloud.com/pastebin/HyBOx2gA/
<dholbert> miriam: I like how clean and simple and unrepetitive it is
<dholbert> astearns: do you always get extra indentation, or is it possible to have a bare selector and then its decl block indented once?
<dholbert> fantasai: it's up to your own indentation file & how you organize your code
<dholbert> fantasai: the idea is, inside a selector, you put open and close curly braces in the same spot where you would put a declaration
<dholbert> fantasai: and that indicates you're going to put some style rules
<TabAtkins> https://www.irccloud.com/pastebin/nakoTt1z/
<dholbert> fantasai: and that's important for selector parsing [...]
<miriam> the proposal we made in this comment: https://github.com/w3c/csswg-drafts/issues/4748#issuecomment-924118287
<dholbert> fantasai: at the top of the proposal, there was a suggestion to use parenthesis
<dholbert> fantasai: if you're not mixing declarations and style rules in the same selector block, you can just double-up your curly braces and indent one level
<dholbert> fantasai: and that's pretty reasonable, but if you're mixing, you probably want to indent two levels for the case where you're including a selector
<fantasai> s/of the proposa/of the issue/
<fantasai> s/parenthesis/parenthesis, which is roughly what we're proposing here, except using curly braces/
<dholbert> astearns: so we have two competing ideas
<TabAtkins> (I find all the ways to avoid extra indents being easy to mess up, especially when editting later.)
<dholbert> astearns: I like the idea of having a single syntax, not an at-rule vs ampersand depending on syntax
<dholbert> astearns: sounds like either we use at-rule everywhere or open/close braces everywhere
<dholbert> astearns: not hearing consensus, I'm hearing "I could live with"
<dholbert> TabAtkins: I really really don't like the double-indent
<dholbert> TabAtkins: people can't easily adjust indentation files without messing with other code
<dholbert> TabAtkins: [...]
<dholbert> astearns: one argument for the bracketed version: it's easy to see what's being declared at each level of indentation
<dholbert> astearns: you can read down a column and it's all the same sort of thing at a given level of indentation
<dholbert> fantasai: if we don't just do curly-braces, you'll need to prefix every single selector with somehting
<dholbert> fantasai: don't want that to be @nest; that's too noisy
<dholbert> fantasai: single symbol is not so bad
<florian> I'm mildly in favor of the braced syntax, but I don't feel terribly strongly either way
<heycam> q+
<dholbert> fantasai: nesting another block in there is the right way to go
<TabAtkins> ack
<TabAtkins> zakim, ack
<Zakim> I don't understand 'ack', TabAtkins
<TabAtkins> ack TabAtkins
<dholbert> heycam: agree with fantasai
<astearns> ack heycam
<dholbert> heycam: indentation is least important consideration of the things we're considering
<dholbert> heycam: we often add @media or @supports rules and don't worry about additional indentation there
<dholbert> heycam: I agree @nest syntax looks pretty noisy
<dholbert> TabAtkins: @media usually isn't nested
<dholbert> TabAtkins: I'm not worried about one level of indentation, but rather 3 levels
<fantasai> s/not so bad/not so bad, but then selector prelude in unnested style rules will be different from nested style rules which is also not great/
<dholbert> TabAtkins: that's enough to trigger lint warnings in many systems
<dholbert> astearns: I have a terrible idea
<dholbert> astearns: each nested selector could start with three colons
<dholbert> astearns: we've only used 2 so far!
<dholbert> astearns: [sarcasm, I think :D ]
<dholbert> astearns: anyone who wants to publicly argue against bracket syntax? (aside from TabAtkins )
<dholbert> astearns: small number of folks on call, but let's try a small straw poll
<miriam> {}
<TabAtkins> @nest
<smfr> {}
<fantasai> {}
<astearns> {}
<florian> {}
<vmpstr> {}
<heycam> {}
<hober> {}
<dholbert> {}
<dholbert> astearns: are you ok with brackets, TabAtkins ?
<dholbert> TabAtkins: I really want to show a normal stylesheet and how hugely indented it gets
<dholbert> TabAtkins: happy to take a provisional resolution for now
<dholbert> astearns: what does SASS use?
<dholbert> TabAtkins: nothing, you just directly nest them in. Three deep = three indents
<dholbert> astearns: and that's not possible for us?
<dholbert> TabAtkins: there are certain selectors you can construct that look like property declarations
<dholbert> TabAtkins: requires too much lookahead for UAs to implement that
<astearns> ack fantasai
<emeyer> +1 to seing side by side code examples
<dholbert> astearns: should be a note in the spec
<dholbert> astearns: proposed resolution: change nesting draft to use open/close brackets, and add a note to show that syntax vs. what Tab would prefer
<dholbert> RESOLVED: change nesting draft to use open/close brackets, and add a note to show that syntax vs. what Tab would prefer
<dholbert> fantasai: we also should resolve to have a single syntax for nesting
<dholbert> RESOLVED: our preference is for a single syntax for nesting
<dholbert> TabAtkins: one final question in this: right now, the spec defines the nested style rules to use a different OM class. the css nesting rule interface
<dholbert> TabAtkins: if we're using a single syntax, then I don't think that makes a lot of sense?
<dholbert> TabAtkins: they are in every possible way equivalent to a style rule
<dholbert> TabAtkins: so my proposal is to just switch to use the existing CSS Style Rule interface in cssom
<dholbert> astearns: so the only change is to change cssom to allow style rule to contain a list of style rules
<dholbert> astearns: proposed resolution: not have nesting om using its own interface; instead, just allow style rule to contain a list of style rules
<dholbert> RESOLVED: don't have nesting om using its own interface; instead, just allow style rule to contain a list of style rule
tabatkins commented 2 years ago

Here's a slightly non-trivial example of Nesting usage, taken from Bootstrap, using just two levels of nesting:

Sass:

.table-bordered {
    > :not(caption) > * {
        border-width: $table-border-width 0;

        > * {
            border-width: 0 $table-border-width;
        }
    }
}

Here's it in the two modes:

{}-wrapped:

.table-bordered {
    {
        & > :not(caption) > * {
            border-width: $table-border-width 0;

            {
                & > * {
                    border-width: 0 $table-border-width;
                }
            }
        }
    }
}

@nest-prefixed:

.table-bordered {
    @nest & > :not(caption) > * {
        border-width: $table-border-width 0;

        @nest & > * {
            border-width: 0 $table-border-width;
        }
    }
}

Y'all are really okay with those five levels of braces? That looks good to y'all? If they'd gone down three levels, which is the limit that most styles guides recommend, it would be seven.

And yes, there are ways to format that to reduce the amount of indent, but they're either fragile or just as much effort as @nest.

If you go with "double-brace if you're just doing nesting", then you end up with:

```css
.table-bordered {{
    & > :not(caption) > * {
        border-width: $table-border-width 0;

        {
            & > * {
                border-width: 0 $table-border-width;
            }
        }
    }
}}

But now you've got inconsistent styles even within this one rule, since the second level does apply a property and thus needs to brace normally. It also breaks the benefit cited by someone in the call (not captured in the minutes) of having even/odd indents indicate properties vs rules, since here both 0 and 1 indents are rules. Finally, now you've got non-local edits to make if you do decide that .table-bordered needs to apply some properties, since you now have to put the inner braces back on their own line and increase the indent of the entire rule, which seems both mildly annoying and easy to forget to do (especially if people are coming from a processor language that already support intermixed properties and nested rules, so it looks fine to their instincts).

Alternately, one can tight-wrap each individual nested rule:

.table-bordered {
    {& > :not(caption) > * {
        border-width: $table-border-width 0;

        {& > * {
            border-width: 0 $table-border-width;
        }}
    }}
}

This avoids excessive indents, but now you're still having to prefix every nested rule with something (a single { rather than @nest), and you have to end the nested rules with the weird-to-my-CSS-eyes }}. At least my editor (Sublime) handles it well by default, but it still has an extremely weird smell to it. (And @LeaVerou's complaint about selector lists feeling odd would still apply here, since only the first selector in the list would have the { prefix.) (It also suffers from the other complaint someone voiced during the meeting, that the @nest syntax isn't valid at top-level. This isn't either.)

Finally, both of these are just styles that teams need to adopt, both of which are foreign to current CSS, all preprocessors, and every non-CSS braces-using programming language I'm familiar with. Nothing prevents people from continuing to use it in the double-indent, giant-stack-of-close-braces-for-moderate-nesting style shown in the first block at the top of this post, which is indeed exactly how that bracing style would be indented by default in most style guides I've ever seen.

Prefixing with @nest is a little different than what current preprocessors do, but using/formatting it according to standard CSS practices produces an easy to read and write appearance that should feel familiar to authors. If we really feel like the six characters of @nest-followed-by-a-space are that burdensome, we can shorten it to @n or something.

I just feel like trying to go with this extra-braces style will end up being a major mistake that we severely regret in the long term (and heck, probably in the short term, too). It's trying to optimize away something that's not that bad with something that's much, much worse.

tabatkins commented 2 years ago

Just for some comparison, here's another real-world example using two levels of Nesting, but having more properties mixed in, in case that changes the visual weights of things:

Sass:

figure {
    border: 1px solid transparentize($grey, 0.69);
    border-radius: 0.23rem;
    display: flex;
    flex-direction: column;
    margin: 0 auto;
    margin-bottom: 0.23rem;
    padding: 0.46rem;
    text-align: center;

    > audio {
        order: 2;
        width: 100%;

        + figcaption {
            order: 1;
            margin-bottom: 0.23rem;
            margin-top: 0;
            text-align: left;
        }
    }

    > figcaption {
        color: #666;
        font-size: 0.69rem;
        margin-top: 0.69rem;
    }

    > video {
        + figcaption {
            margin-top: 0;
        }
    }

    + figure {
        margin-top: 0.46rem;
    }
}

{}-wrapped:

figure {
    border: 1px solid transparentize($grey, 0.69);
    border-radius: 0.23rem;
    display: flex;
    flex-direction: column;
    margin: 0 auto;
    margin-bottom: 0.23rem;
    padding: 0.46rem;
    text-align: center;

    {
        & > audio {
            order: 2;
            width: 100%;

            {
                & + figcaption {
                    order: 1;
                    margin-bottom: 0.23rem;
                    margin-top: 0;
                    text-align: left;
                }
            }
        }

        & > figcaption {
            color: #666;
            font-size: 0.69rem;
            margin-top: 0.69rem;
        }

        & > video {
            {
                & + figcaption {
                    margin-top: 0;
                }
            }
        }

        & + figure {
            margin-top: 0.46rem;
        }
    }
}

@nest-prefixed:

figure {
    border: 1px solid transparentize($grey, 0.69);
    border-radius: 0.23rem;
    display: flex;
    flex-direction: column;
    margin: 0 auto;
    margin-bottom: 0.23rem;
    padding: 0.46rem;
    text-align: center;

    @nest & > audio {
        order: 2;
        width: 100%;

        @nest & + figcaption {
            order: 1;
            margin-bottom: 0.23rem;
            margin-top: 0;
            text-align: left;
        }
    }

    @nest & > figcaption {
        color: #666;
        font-size: 0.69rem;
        margin-top: 0.69rem;
    }

    @nest & > video {
        @nest & + figcaption {
            margin-top: 0;
        }
    }

    @nest & + figure {
        margin-top: 0.46rem;
    }
}

As an observation, modifying the Sass to @nest was trivial; I had to add & to every selector anyway, and typing an @nest in front at the same time was fine. Modifying to the brace-wrapped actually took a little bit of work, and I had to double-check that I'd actually done so correctly afterwards, since it wasn't obvious from trivial inspection that I'd caught all the nesting. This plays into my "feels familiar" point, since authors will be able to write their CSS the exact same way they do their current SCSS, just with a small prefix on the rules.

vrubleg commented 2 years ago

It looks like the people who voted for additional level of indentation just didn't use nesting in SCSS and don't know how deep it can be. It is always better to avoid too much of indentation if there are no good reasons for it.

As an example, I have received notifications from this issues with messages from @tabatkins. The examples which used @nest looked nice on my phone, I could read it. The ones with additional levels of nesting looked unreadable because of too many line breaks.

Yeah, people usually read code on big screens. But sometimes you may receive some piece of code by email (a notification from GitHub, or a message from mailing list). Too much of indentation is not acceptable in such cases.

TabAtkins: there are certain selectors you can construct that look like property declarations TabAtkins: requires too much lookahead for UAs to implement that

I don't get it. Nesting supposed to be used everywhere in a few years after it is available in all modern browsers. It is really a new major feature in CSS. And browser developers don't want to update their parsers to make the most convenient syntax possible? It is not that hard to implement look ahead to check if it looks like a property or not. It is worth to spend some time to implement it. It is quite an important thing, and it will be used by millions of developers.

If it not feasible, the solution with @nest before every rule is much better than additional level of nesting. One day in the future, browser developers probably will be willing to update their CSS parsers to support the most convenient SCSS-like syntax. All that would be required in this case is to allow implicit @nest for all cases, no any other changes in syntax.

Oh... Just dreaming.

meyerweb commented 2 years ago

I’d be even happier with:

.table-bordered {
     @^ & > :not(caption) > * {
          border-width: $table-border-width 0;

          @^ & > * {
               border-width: 0 $table-border-width;
          }
     }
}

…or something to that effect, because while I appreciate the reduction of the brackets and the way syntax coloring makes all the @nests stand out to give me a hook on which to hang quick understanding of the structure, I also don’t want to have to type it over and over and over and over again. And @^ is a lot quicker and easier to type than @nest.

NOVALISTIC commented 2 years ago

I've never used a preprocessor in a real-world project so my opinion offers either a unique perspective or absolutely no value whatsoever to this discussion; that said, I also dislike the double layers of nesting and don't want to have to hypothetically fight my editor to get it not to indent the contents of nesting blocks just to avoid that (whether they start with { or @nest {).

I don't even want @nest to be shortened to @n. @nest communicates its functionality clearly and unambiguously, and if we're talking about saving keystrokes a reasonably advanced IDE could autocomplete or expand @n to (with appropriate indentation level)

@nest <selector> {
    <declarations>
}

I like the nesting-by-combinator syntax that @mirisuzanne and @fantasai came up with, and would settle for the additional indentation only for that use case, because at least the indentation has a functional purpose and I'm not just doubly indenting things for the sake of it. Allowing the option to group several nested rules into one @nest block would be fine too but I don't know if that's effectively like returning to having two syntaxes.

The only aspect of nesting that could be a problem to me is getting lost in a long (often top-level) series of nested rules, a common problem in many block-based programming languages and is somewhat alleviated with strategically placed comments, and a problem that is completely unrelated to this thread anyways.

tabatkins commented 2 years ago

(Full disclosure: I asked for comments on Twitter. I tried to be as even-handed as I could be.)


And browser developers don't want to update their parsers to make the most convenient syntax possible? It is not that hard to implement look ahead to check if it looks like a property or not. It is worth to spend some time to implement it. It is quite an important thing, and it will be used by millions of developers.

It's not that "they don't want to". It's that everything browsers do with CSS is under extremely tight perf constraints, and arbitrary lookahead hurts that, when the entire rest of CSS can be parsed with a single token of lookahead (at the structural level, at least). And this is especially problematic when the normal behavior for most properties is to be ambiguous; every color:red declaration looks like a selector (matching a <color> element with a :red pseudo-class) until the ending ;.

Preprocessors can happily eat the perf hit, because it's insignificant for their use-cases.

johannesodland commented 2 years ago

I agree with @tabatkins. Wrapping nested properties in brackets will add one extra level of indentation per nesting layer. This will add up to too many layers of indentation and will make our stylesheets hard to read.

We've been using nesting through postcss for years, and I much prefer the previous syntax.

Are you sure the 'dual' syntax is too hard for authors? It's in use by a large number of authors already, without causing issues. Most nested rules are simply prefixed with & and @nest is only needed if you need nesting as a descendant.

Shouldn't users of the existing syntax count for something? https://www.npmjs.com/package/postcss-nesting

I also wonder how will this new syntax will work with nested media-queries. Will they have to be wrapped in an extra layer of brackets as well?

proimage commented 2 years ago

@tabatkins Seeing as you've been the most vocal opponent of bracketed nesting (and I don't blame you in the slightest), I'd be interested to hear your thoughts on my last-minute idea, quoted below... it seems to have gotten missed in all the hubbub. Thanks!

I'd be happy with that solution as well. Just to make sure we cover all bases though, I've got one more syntax proposal that sort of builds on that, but also uses a pattern I haven't seen anyone raise yet. It avoids double-indenting and allows bulk-nesting in one diabolical swoop:

ul.parent {
    padding-left: 0;
}, @nest { // or perhaps [ or (

    // ul.parent li.child
    li.child {
        list-style-type: none;
    }

    // .wysiwyg ul.parent
    .wysiwyg & {
        list-style-type: initial;
    }
}
tabatkins commented 2 years ago

@proimage Not a fan - it means the "nested" rules aren't actually nested into the "parent" rule at all. It's also very far from any existing patterns, which is one of my complaints about the extra-brackets syntax too.

vrubleg commented 2 years ago

If {{ and }} are allowed, too much of indentation can be avoided this way:

.parent {{
    & {
        color: black;
    }

    & > div {
        color: red;
    }

    .super & {
        color: green;
    }
}}

What if we won't allow mixing properties and nesting in one block, so using & {} would be required if this block is used for nesting of other things? In this case we even could just use { and } instead of the {{ and }}, and distinguish between properties-only and nesting-only blocks by the first token in the block (if it is &, current block is a nesting-only block)?

.parent {
    & {
        color: black;
    }

    & > div {
        color: red;
    }

    .super & {
        color: green;
    }
}

Just an idea.

LeaVerou commented 2 years ago

After giving this a lot of thought over the last couple of weeks, I think I agree with @tabatkins' argument that we should strive to avoid nesting introducing a new level of indentation. 4 levels of nesting are very common for any medium sized stylesheet, and that would make them 8 with this kind of syntax.

I also don't like solutions where symbols are used with a non-obvious meaning (e.g. the @^ idea above). They are hard to remember and hard to google for.

Repeating @nest over and over is a no-no, these nested selectors are very common and this would make stylesheets annoying to type and difficult to read.

Therefore, I think the current syntax is the lesser of many evils, even though it has its own problems (two different syntaxes for no obvious (to authors) reason).

Question: These selectors that can be written to resemble declarations, how commonly are they actually used? It's basically only element selectors, followed by pseudos, with the & somewhere after the first pseudo, right? Can we get any statistics on the prevalence of these in nested selectors? Are we designing syntax around a theoretical case that is never actually needed in practice? What if we ditch @nest and just make these selectors parse errors? Sure, it's a terrible experience if you craft a selector that ends up being a parse error for no discernible reason, but if these are so rare that only a tiny percentage of authors will ever stumble on one, it may be a reasonable price to pay for having a nice general syntax without the duality of @nest vs no @nest. It's all about which compromises we make, it's clear at this point that there is no solution here that doesn't involve compromises. While it would definitely be an ugly wart, I'm much more comfortable with one author having to do some digging to find this footnote because their weird selector didn't work than every author having to think "Do I use @nest here or not?" for every single nested selector they type. Of course it all depends on actual frequency of these selectors. I could contact the HTTPArchive folks and get some stats if this idea might be workable.

proimage commented 2 years ago

Final idea from me, I think. I daresay that it might give us the best of both worlds... 🤷‍♂️

ul.parent {
    padding-left: 0;

    /*
        Inline nesting declarations can stay like they are now (or be reduced 
        to a single syntax where `@nest` is a required prefix, whatever)
    */
    & li.child { list-style-type: none; }
    @nest .wysiwyg & { list-style-type: initial; }

    /*
        Block nesting has explicit and unique start and end keywords. Also, it
        doesn't use brackets, so there's no particular need to indent the block.
    */
    @nestStart
    /* ul.parent li.child */
    li.child {
        list-style-type: none;
    }

    /* .wysiwyg ul.parent */
    .wysiwyg & {
        list-style-type: initial;
    }
    @nestEnd
}
nightpool commented 2 years ago

Is there an issue with choosing one of either @nest and &, and requiring that &/@nest starts every nested selector list? Supporting selectors that don't start with &, like .wysiwyg &, seems like a really niche use-case, based on my experience with pre-processors (I would be surprised to find any major project that used it extensively).

That is, why not just take

figure {
    border: 1px solid transparentize($grey, 0.69);
    border-radius: 0.23rem;
    display: flex;
    flex-direction: column;
    margin: 0 auto;
    margin-bottom: 0.23rem;
    padding: 0.46rem;
    text-align: center;

    > audio {
        order: 2;
        width: 100%;

        + figcaption {
            order: 1;
            margin-bottom: 0.23rem;
            margin-top: 0;
            text-align: left;
        }
    }

    > figcaption {
        color: #666;
        font-size: 0.69rem;
        margin-top: 0.69rem;
    }

    > video {
        + figcaption {
            margin-top: 0;
        }
    }

    + figure {
        margin-top: 0.46rem;
    }
}

and translate it to either

figure {
    border: 1px solid transparentize($grey, 0.69);
    border-radius: 0.23rem;
    display: flex;
    flex-direction: column;
    margin: 0 auto;
    margin-bottom: 0.23rem;
    padding: 0.46rem;
    text-align: center;

    & > audio {
        order: 2;
        width: 100%;

        & + figcaption {
            order: 1;
            margin-bottom: 0.23rem;
            margin-top: 0;
            text-align: left;
        }
    }

    & > figcaption {
        color: #666;
        font-size: 0.69rem;
        margin-top: 0.69rem;
    }

    & > video {
        & + figcaption {
            margin-top: 0;
        }
    }

    & + figure {
        margin-top: 0.46rem;
    }
}

(if you like sigils)

or

figure {
    border: 1px solid transparentize($grey, 0.69);
    border-radius: 0.23rem;
    display: flex;
    flex-direction: column;
    margin: 0 auto;
    margin-bottom: 0.23rem;
    padding: 0.46rem;
    text-align: center;

    @nest > audio {
        order: 2;
        width: 100%;

        @nest + figcaption {
            order: 1;
            margin-bottom: 0.23rem;
            margin-top: 0;
            text-align: left;
        }
    }

    @nest > figcaption {
        color: #666;
        font-size: 0.69rem;
        margin-top: 0.69rem;
    }

    @nest > video {
        @nest + figcaption {
            margin-top: 0;
        }
    }

    @nest + figure {
        margin-top: 0.46rem;
    }
}

(for those that don't like sigils)

LeaVerou commented 2 years ago

@proimage

That's basically my suggestion from https://github.com/w3c/csswg-drafts/issues/4748#issuecomment-930472123 except it also includes an unneeded @nestEnd token (which is not needed if you can't intersperse nested selectors with declarations).

@nightpool Selectors that don't start with & are pretty common, though it gets weird with multiple nesting levels:

.foo {
    .bar & { /* Ok, .bar .foo */
        .baz & { /* .baz .bar .foo ? How do we do .foo .baz .bar? */
        }
    }
}
proimage commented 2 years ago

@proimage

That's basically my suggestion from https://github.com/w3c/csswg-drafts/issues/4748#issuecomment-930472123 except it also includes an unneeded @nestEnd token (which is not needed if you can't intersperse nested selectors with declarations).

Well, you had a great suggestion then. ;) It gets my vote.