Skip to content

[selectors-4] need "first-matching-sibling" combinator #3813

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

Open
v-python opened this issue Apr 8, 2019 · 8 comments
Open

[selectors-4] need "first-matching-sibling" combinator #3813

v-python opened this issue Apr 8, 2019 · 8 comments

Comments

@v-python
Copy link

v-python commented Apr 8, 2019

The current sibling combinators are:

  • select immediately following sibling if it matches
    ~ select any following siblings that match

Neither of these can be used to select the first following sibilng that matches. Or the 2nd. I only need the first, in my current application, but could imagine that someone might need the Nth.

This type of selection becomes useful when dealing with a grid containing hundreds or thousands of child elements of different types or classes. Yep, I have such. So the relationships between siblings become far more important when siblings become far more numerous.

It doesn't seem that one can apply the currently defined :first-* to the subset of siblings selected by ~, as those all apply based on parentage, not a selected subgroup.

Some syntax ideas:

A: S1 ~1 S2
S1 ~2 S2
...

B: some other special character than ~ to just mean first, if Nth doesn't sound useful.

C: some new pseudo-class that selects among a group :first-that-matches( selector ), nth-that-matches( selector ), :last-that-matches( selector ), :nth-last-that-matches( selector ). In this syntax, one would write S1 ~ :first-that-matches( S2 )

D: S1 ~:first S2
S1 ~:nth( n-expression ) S2
S1 ~:last S2
S1 ~:nth-last( n-expression ) S2

@MelvinIdema
Copy link

MelvinIdema commented Apr 8, 2019

What is the difference between ~N or the already existing div ~ p:nth-of-type(N)?

@v-python
Copy link
Author

v-python commented Apr 8, 2019

<div><p id=p1><p id=p2><p id=p3><p id=p4></div>

.p2 ~2 p would choose .p4
.p2 ~ p:nth-of-type(2) would not choose anything, because the 2nd child of the parent is .p2, which is not its own sibling.

~N chooses the Nth matching sibling, not the sibling that happens to be Nth child of its parent, if that Nth child happens to also be a sibling of the left element.

@AmeliaBR
Copy link
Contributor

AmeliaBR commented Apr 8, 2019

In many cases, this could be handled by the "nth of" syntax. :nth-child(1 of .sibling ~ .target) would select the first child element with class target that is also a following sibling of an element with class sibling. (Note, this syntax, with a complex selector in the of clause, has been deferred to selectors level 5 in #3760.)

However, that selector would fail if you had more than one sibling reference element within the same parent, and you wanted to select the first target element after each. (e.g., If you have a document that uses headings but no explicit <section> elements, and you want to select the first <p> paragraph after each heading, skipping over any <figure> or <div> or other sibling element that might be in between the heading and the paragraph.)

To get the behavior that @v-python wants, the "nth of" counting would need to be applied to each sibling combinator, rather than counting across all children of a parent that also happen to match the any-sibling selector.

Modifying the ~ combinator directly is also likely to be problematic, because it's not clear how much of the right hand side of the combination gets taken into consideration when doing the counting. E.g., what if S1 ~2~ S2 is .sibling ~2~ .target:hover? Do you only count target elements that are being hovered?

So I'd lean towards extending the :nth-child(n of selector) approach to :nth-sibling(…). The parenthesized expression clearly limits the scope from which you're counting.

But, extending the syntax to sibling relationships isn't straightforward. The scope of "child" is defined by the DOM tree, but the scope of "sibling" needs to be assessed relative to the previous part of the selector. We'd need something that's logically more like :nth(n of .target)-sibling(of .sibling) (which is awful and not something I'm suggesting. I'm just trying to show the structure — you need to include both the sibling and target selector in the same place you define the counting rule).

Maybe :nth-sibling(1 of .sibling / .target) ?

@v-python
Copy link
Author

v-python commented Apr 8, 2019

Thanks for the response, Amelia. My use case isn't one of the "many" you refer to... There could be dozens of items that match the left side, each of which would want to find the first sibling that matches the right side, of which there could be dozens or even hundreds following. So your first pararaph wouldn't apply immediately, and your second is a good example of why.

Interesting comment regarding how much of the right hand side applies: my use case is all on the same level, but now that you mention it, I can see it certainly could be an issue of understanding or documenattion to define the binding. + doesn't look very far, binds to only one element, and either matches or doesn't based on the rest of the RHS. ~ starts with a set (that happens to also be a list), and can just start tossing items that don't match based on the rest of the RHS. But now I understand why all the current :nth-* items modify a particular selector, instead of using a "rich combinator" type of syntax...

So another syntax possibliity could be

E: S1 ~ S2:nth-match( n-expression )

This would clearly define the set of S2 that follows S1 as now, but would limit the scope of the counting to S2, not further to the RHS. The remainder of the RHS would simply choose whether to apply the rule or not, having identified the candidate sibling, among the set identified by the (unchanged for this syntax) ~ combinator. Does that simplify the presentation to the user, as well as limit the scoping on the RHS?

@johannesodland
Copy link

johannesodland commented Aug 15, 2022

Having a :nth-sibling() selector would be really nice.

Say you wanted to style every first paragraph after a heading, even if there is other elements between:

/* Using Amelias suggested syntax above */
:nth-sibling(1 of h2 / p) {
   …
}
<h2>Heading</h2>
<img />
<p>This paragraph should get a custom style.</p>
<p>This paragraph should be unaffected.</p>

It would also be useful if it was possible to limit how far the selector applies with another selector and not just an+b.

Say that you want to apply different styles to elements after even and odd headings:

/* The sibling selector applies only until the next heading */
:nth-sibling(n of :nth-child(odd of h2) / p until h2) {
   color: green;
}
:nth-sibling(n of :nth-child(even of h2) / p until h2) {
   color: blue;
}
<h2>Odd Heading</h2>
<img />
<p>Paragraphs after odd headings should be green.</p>
<p></p>

<h2>Even Heading</h2>
<img />
<p>Paragraphs after even headings should be blue.</p>
<p></p>

@kizu
Copy link
Member

kizu commented Dec 25, 2023

I just wrote an article related to this: https://blog.kizu.dev/nth-sibling-christmas-tree/

The need for this kind of selector is something I stumble upon regularly, and it could be really nice if there would be some way of achieving it, be it via a new pseudo-class like :nth-sibling(), some modification of :nth-child(), or anything else. For now, the only workarounds require repeating ourselves. CSS Nesting alleviates some of the repetition, but not all of it, and can potentially be not good for performance, so having something available natively will be really good.

@johannesodland
Copy link

johannesodland commented Dec 26, 2023

Note that there is now a separate issue for a sibling scope at-rule named @scope-siblings: #7751

If :nth-sibling() is limited to be used in a sibling-scope that would simplify the syntax, as the first siblings are then selected in the at-rule prelude:

@scope-siblings (.sibling) {
  :nth-sibling(1 of .target) {…}
}

:nth-sibling() could also be useful as a scope limit selector:

@scope-siblings (.sibling) to (:nth-sibling(5)) {
  …
}

@johannesodland
Copy link

When it comes to use cases, I think the arguments for having :nth-sibling() are the same as for having :nth-child().

With :nth-child() you can target element children by their index. With :nth-sibling() it would be possible to target a sibling within a group of siblings. There are many cases where a group of elements are not segmented by a separate parent, but by other siblings. In example, in an article, elements are often segmented by headings and not by structuring them as children in separate elements.

If you need to apply custom styling to the first and/or last paragraph of an element? Use :nth-child().
If you need to apply custom styling to the first and/or last paragraph of a chapter, then :nth-child() cannot be used. This is where an :nth-sibling() becomes useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants