Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[selectors-4] [css-nesting-1] pseudo elements need to be clarified to correctly handle relative selectors. #7979

Closed
romainmenke opened this issue Oct 29, 2022 · 19 comments
Labels
Closed as Question Answered Used when the issue is more of a question than a problem, and it's been answered. css-nesting-1 Current Work

Comments

@romainmenke
Copy link
Member

romainmenke commented Oct 29, 2022

see : https://www.w3.org/TR/selectors-4/#issue-5830d0c1

ISSUE 1 : Pseudo-elements aren’t handled here, and should be.

Relative selectors begin with a combinator, with a selector representing the anchor element implied at the start of the selector. (If no combinator is present, the descendant combinator is implied.)

Pseudo elements are not allowed in :has, so this issue didn't need to be resolved before : #7463

/* ambiguous but invalid */
:has(::before) {}

With nesting you can now write :

.foo {
  ::before {}
}

Is that (1) :

.foo::before{}

or (2) :

.foo ::before{}

I would say it is 1.
::before starts a new compound selector and has its own implicit "into pseudo tree" combinator as illustrated with .foo::before:hover vs. .foo:hover::before.

@bradkemper
Copy link
Contributor

Number 2. The space is implied by the lack of a & at the beginning of the line. It would be confusingly inconsistent otherwise.

@romainmenke
Copy link
Member Author

romainmenke commented Oct 29, 2022

The space is only implied when the selector doesn't start with a combinator :

.foo {
  .bar {} /* implied space */

  > .bar {} /* no implied space */
}

So it depends on what ::before is.
This is an old issue.

also see : #7346 (comment)

In particular, it would allow us to finally revise the data model of pseudo-elements to make some dang sense when combined with other features like relative selectors and nesting, which makes me very happy. The foo::bar syntax will finally no longer be considered a weird compound selector; it's just a legacy way to write the complex selector foo :> bar, with a proper combinator and everything.

@romainmenke
Copy link
Member Author

@sesse I noticed that Chrome currently does (2).
Was there a specific reason for this? Maybe I overlooked something.

Screenshot 2022-11-01 at 10 43 25

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Document</title>
	<style>
		body {
			::before {
				content: "";
				position: fixed;
				left: 50px;
				right: 50px;
				width: 100px;
				height: 100px;
				background-color: red;
			}
		}
	</style>
</head>
<body>
	<div></div>
</body>
</html>

equivalent :

body {
	& ::before {
		content: "";
		position: fixed;
		left: 50px;
		right: 50px;
		width: 100px;
		height: 100px;
		background-color: red;
	}
}

@sesse
Copy link
Contributor

sesse commented Nov 1, 2022

@sesse I noticed that Chrome currently does (2). Was there a specific reason for this? Maybe I overlooked something.

I didn't do anything specific for it; it's just that ::before isn't parsed as a combinator.

So if you look as the selector ::before, the spec (css-nesting-1) says we are to interpret these as a relative selector (since there is no & in it). This is the definition of a relative selector: https://csswg.sesse.net/selectors-4/#relative

More specifically: “Relative selectors begin with a combinator, with a selector representing the anchor element implied at the start of the selector. (If no combinator is present, the descendant combinator is implied.)”

These are the combinators: https://csswg.sesse.net/selectors-4/#typedef-combinator

<combinator> = '>' | '+' | '~' | [ '|' '|' ]

So ::before isn't a combinator, and thus a descendant combinator is simplied, and the rule is equivalent to & ::before, not &::before. If you want some other behavior, you'll probably need to change the language to not be in terms of relative selectors. But I'm not an expert here :-)

@romainmenke
Copy link
Member Author

romainmenke commented Nov 1, 2022

Yup, that is all correct.
I opened this issue because I think there is some ambiguity between the wording and the intention. (I might be wrong about that.)

.foo::before is not a a single compound selector where both .foo and ::before are "attributes" of the same element. .foo is a class on one element and ::before is a pseudo element. This makes .foo::before more like a complex selector.

This is also clear from .foo::before:hover vs. .foo:hover::before.

  • Is there an implicit combinator involved with pseudo elements?
  • If there is an implicit combinator how does it affect relative selectors?

This is related to : #7346

It'd be great if we had a way to achieve this, such as:

:> - select child pseudo elements. As in, div :> before :> marker would be equivalent to div::before::marker
:>> - select descendant pseudo elements. As in, div :>> marker would enable the use-case above.

If such a combinator is ever introduced we would have a difference between :

.foo {
  /* a true combinator -> .foo::before */
  :> before {}

  /* implied combinator -> .foo ::before */
  ::before {}
}

@romainmenke
Copy link
Member Author

@tabatkins @argyleink ping

@sesse
Copy link
Contributor

sesse commented Nov 1, 2022

I opened this issue because I think there is some ambiguity between the wording and the intention. (I might be wrong about that.)

I honestly don't know. I don't find any of this behavior intuitive, but I've been told it is if I only knew Sass. :-)

I guess my best explanation here is that ::before is equivalent to *::before, and I don't think anyone would say that *::before should be the same as &::before, but rather & *::before.

@cdoublev
Copy link
Collaborator

cdoublev commented Nov 1, 2022

type::pseudo matches <compound-selector> so if your parser is grammar-driven, this is what you have. But I understand that it can/should be considered as a kind of complex selector because ::pseudo does not qualify type but selects a pseudo element that is bound to type (which is not the same as selects a child element, ie. a descendant combinator).

@romainmenke
Copy link
Member Author

romainmenke commented Nov 1, 2022

but I've been told it is if I only knew Sass.

Ha, same here :)
My main experience with nesting is from implementing it as PostCSS transforms.

I think this issue also illustrates how messy implicit things can become.
People from different experiences will have different expectations.


I don't have a preference between 1 or 2.
But I think it needs to be clarified in the selector module just to be sure.

@sesse
Copy link
Contributor

sesse commented Nov 1, 2022

It makes sense if you know it's defined in terms of relative selectors, but I really doubt that's a term most authors will know. E.g., if you make a web search for “relative selector css”, the only two hits among the top 10 that understand the term are MDN and the spec itself. Every other video, tutorial page etc. thinks it's something else (e.g. mixing it up with compound selectors). So this isn't a concept that we can hang authors' intuitions on.

@cdoublev
Copy link
Collaborator

cdoublev commented Nov 1, 2022

but I've been told it is if I only knew Sass.

Ha, same here :)

+1. EDIT: I mean, I was confused by div { .class {} } being equivalent to div .class {} instead of div.class {}.

@Loirooriol Loirooriol added the css-nesting-1 Current Work label Nov 2, 2022
@Loirooriol
Copy link
Contributor

If the relative selector ::before should be treated as *::before, then :has(::before) should be treated as :has(*::before) (and analogous for all pseudo-elements). But #7463 resolved:

allow future pseudo-elements to define that they are valid if useful/possible.

And the most common usecase seems to check if the element itself has the pseudo-element (and not some random descendant).
So the only options seem:

  1. Let & or :scope inside :has() refer to the anchor element. This increases invalidation complexity and then it's difficult to implement ([selectors-4] Consider disallowing :scope inside :has() #7211)
  2. Add :> as a pseudo-element combinator ([selectors] Child & descendant pseudo element combinators #7346)
  3. Let ::before and *::before have different meanings in relative selectors.

1 and 2 are hypothetical so I'm assuming 3. Thus ::before and *::before can also have different meanings in nesting.

Should they? I dunno, I don't use Sass either, so allowing relative nested selectors that don't start with an explicit combinator is completely confusing to me.

@sesse
Copy link
Contributor

sesse commented Nov 2, 2022

Empirically, Sass treats div { ::before { … }} as div ::before, from what I can see.

@romainmenke
Copy link
Member Author

romainmenke commented Nov 2, 2022

I guess my best explanation here is that ::before is equivalent to *::before

That (I think) is the best way to look at it.

::before has an implicit *, so it doesn't start with an implicit combinator.
this is not going to get easier

::before is equivalent to * :> before not to :> before from #7346

  • ::before -> *::before
  • .foo { ::before {} } -> .foo *::before (or just .foo ::before)
  • * :> before -> *::before
  • :> before -> :scope::before
  • .foo { * :> before {} } -> .foo *::before
  • .foo { :> before {} } -> .foo::before

@romainmenke
Copy link
Member Author

We now match Chrome's implementation.

@cdoublev
Copy link
Collaborator

cdoublev commented Dec 14, 2022

I would prefer that option 2 (:>) in Oriol's comment becomes real and that div:has(before) implicitly means div:has(:> before), ie. an implicit pseudo-element descendent combinator would be assumed. So you would have to write .foo { :> before {} } or .foo { &::before {} } instead of .foo { ::before {} }.

@tabatkins
Copy link
Member

Sass's behavior, and Chrome's, is correct. For historical reasons, while the :: in ::before is similar to a combinator, it's not exactly one; this is reflected in the grammar. (And we've finally properly built pseudo-elements into the grammar hierarchy now.) Thus :has(::before) or .foo { ::before {...}} are not ambiguous - the ::before is read as having a descendant combinator before it, exactly the same as :has(.bar) and .foo { .bar {...}} would be. .foo { &::before {...}} is how you indicate you want the ::before pseudo on the parent rule's element, specifically. As Romain (and the spec) say, a bare ::before is defined to be equivalent to *::before.

I wish pseudo-elements didn't have this sort of grammar, but we're stuck with it.

@Loirooriol The ":has-allowed pseudo-elements" concept we've carved out isn't for changing the meaning of something like :has(::before). A future :has(::foo) that we define to be valid will still be equivalent to :has(*::foo); there's currently no way to ask whether the subject element has a specific pseudo. We'd need the proposed :> combinator for that.

@tabatkins tabatkins added the Closed as Question Answered Used when the issue is more of a question than a problem, and it's been answered. label Dec 14, 2022
@tabatkins
Copy link
Member

Closing as Question Answered, as the specs do have a clear answer. :: is not a combinator, so in .foo { ::before {...}} the inner rule's selector is strictly equivalent to .foo *::before.

@romainmenke
Copy link
Member Author

And we've finally properly built pseudo-elements into the grammar hierarchy now.

Thank you for that, it is much clearer now!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Closed as Question Answered Used when the issue is more of a question than a problem, and it's been answered. css-nesting-1 Current Work
Projects
None yet
Development

No branches or pull requests

6 participants