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] New selector based on the amount of child elements #5694

Open
ramiy opened this issue Nov 4, 2020 · 18 comments
Open

[selectors] New selector based on the amount of child elements #5694

ramiy opened this issue Nov 4, 2020 · 18 comments

Comments

@ramiy
Copy link

ramiy commented Nov 4, 2020

Overview

Currently there are no selectors that let us apply a style based on the total amount of direct children the element has.

I can't apply red color if the element that has 2 direct children or green color if it has 3 direct children.

My proposal is to add a new pseudo-class called :nth-children(n) which is based on the existing naming conventions.

Code Example

Lists:

ul:nth-children(2) {   /* only if a list has 2 child elements */
  color: red;
}
ul:nth-children(3) {   /* only if a list has 3 child elements */
  color: green;
}

Tables:

table tr:nth-children(4) {   /* only if a tr has 4 child elements (including <td>, <th> and other elements) */
  background-color: red;
}
table tr:nth-children(5) {   /* only if a tr has 5 child elements (including <td>, <th> and other elements) */
  background-color: green;
}

The Problem with Existing Pseudo-Class

The vast majority of the child element selectors are trying to drill up - the selector applied on the <li> checking against the parent <ul> element.

While my proposal is to drill down (like flex and grid) - the selector applied on the <ul> checking against the amount of child <li> elements.

E:first-child

An E element, first child of its parent.

E:last-child

An E element, last child of its parent.

E:only-child

An E element, only child of its parent.

E:nth-child(n [of S]?)

An E element, the n child of its parent matching S.

E:nth-last-child(n [of S]?)

An E element, the n child of its parent matching S, counting from the last one.

E:empty

An element that has no children (neither elements nor text) except perhaps white space. (This selector is the only selector that applied on the parent element, checking child elements.)

E > F

An F element child of an E element. (This is a general selector to target a specific child element. Doesn't count totals.)

The Solution

As mentioned above, the new selector will behave like flex and grid, it will be applied on the parent element and check the total number of children elements it has.

E:nth-children(n)

An E element, counting total n children.

Usage

Conditional design based on the amount of child elements:

tr:nth-children(1) td {
  color: red;
}
tr:nth-children(2) td,
tr:nth-children(3) td,
tr:nth-children(4) td {
  color: yellow;
}
tr:nth-children(5) td,
tr:nth-children(6) td,
tr:nth-children(7) td {
  color: green;
}

Another example is to use keyword values:

ul:nth-children(even) {   /* 2, 4, 6, etc. */
  color: red;
}
ul:nth-children(odd) {   /* 1, 3, 5, etc. */
  color: green;
}

Use Cases

This can help developers apply conditional design in many case:

  • Style content based on the total amount of paragraphs - too few paragraphs will have red text color, the more paragraphs the text has the greener the text become.
  • Style charts (see ChartsCSS.org) based on the amount of data items (<td> elements) and amount of datasets (<tr> elements).
  • Style different lists <ol>, <ul>, <dl> based on the total amount of list items.
@noamr
Copy link
Collaborator

noamr commented Nov 4, 2020

Not sure if nth-children is a correct term, maybe children-count?

@ramiy
Copy link
Author

ramiy commented Nov 4, 2020

I will accept any other name as long is the functionality accepted.

@Loirooriol
Copy link
Contributor

Loirooriol commented Nov 4, 2020

Related: #4559 (comment) proposes sibling-index(), sibling-count(), child-count(), and tree-depth() function values.

@noamr
Copy link
Collaborator

noamr commented Nov 4, 2020

I think this can be closed as duplicate of #4559

@ramiy
Copy link
Author

ramiy commented Nov 4, 2020

It's not a duplicate! #4559 propose new functional notations (CSS values spec). This proposal is for new pseudo-class (CSS selectors spec). Those are two different things.

We can discuss more to decide which approach is better but those are two different approaches. Each has its own advantages and disadvantages.

Edit: we can also accept both proposals.

@noamr
Copy link
Collaborator

noamr commented Nov 4, 2020

It's not a duplicate! #4559 propose new functional notations (CSS values spec). This proposal is for new pseudo-class (CSS selectors spec). Those are two different things.

Gotcha

@faceless2
Copy link

faceless2 commented Nov 4, 2020

In that case it's the same complexity as :has() - i.e., it's complex (see eg #3345).

:nth-children(2) would be the same as :has(> :nth-child(2)):not(:has(> :nth-child(3))) - although I think it's fair to say the latter isn't quite as obvious.

Edit: not quite true actually, :has descends the tree whereas this wouldn't have to. So no, not as complex.

@ramiy
Copy link
Author

ramiy commented Nov 4, 2020

:nth-children(2) would be the same as :has(> :nth-child(2)):not(:has(> :nth-child(3))) - although I think it's fair to say the latter isn't quite as obvious.

The new :nth-children(2) is the same as :has(> :nth-child(2)):not(:has(> :nth-child(3))), but with a lower specificity.

Just like :only-child is the same as :nth-child(1):nth-last-child(1), but with a lower specificity.

@emilio
Copy link
Collaborator

emilio commented Nov 4, 2020

Yeah, I agree this is not so complicated as :has() because it doesn't descend. However, it's still a bunch of work and cache misses to walk the DOM if there are tons of children, so that's still not great and probably would need similar caches to what :nth-child and co have. Gecko does keep track of the child count), but that includes text-nodes (:

Two things that come to mind:

  • I suspect this is usually not what you want? I suspect you generally want number of rendered elements, which is a lot harder to compute (and define, more generally). Though you could argue that :nth-child() has the same issue.
  • Shadow DOM makes this all a bit more useless (and it wasn't around when :nth-child() and friends came to be). Again, might not be a blocker either, we have tons of other tree-abiding selectors.

Is there any chance you could elaborate of what particular styling changes you'd apply based on the number of child elements? The thing I can think of is stuff like changing the width of the parent based on number or such, but that seems brittle / repetitive and better suited by stuff like flex / grid / tables / inline-block etc...

@ramiy
Copy link
Author

ramiy commented Nov 4, 2020

Is there any chance you could elaborate of what particular styling changes you'd apply based on the number of child elements? The thing I can think of is stuff like changing the width of the parent based on number or such, but that seems brittle / repetitive and better suited by stuff like flex / grid / tables / inline-block etc...

@emilio I have several use cases but I will elaborate on a particular example I am currently working on. I'm trying to create CSS framework turning HTML data <table> into a chart using pure CSS without any JS (for more details checkout ChartsCSS.org).

It is very hard to create a radar chart with pure CSS when you don't know how many <tr> elements the user will provide. To simplify the explanation think of clip-path shapes generated using clippy:

Clippy-CSS-clip-path-maker

/* Triangle */
tbody:nth-children(3) {
  clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
}
/* Rhumbus */
tbody:nth-children(4) {
  clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
}
/* Pentagon */
tbody:nth-children(5) {
  clip-path: polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%);
}
/* Hexagon */
tbody:nth-children(6) {
  clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
}
/* Heptagon */
tbody:nth-children(7) {
  clip-path: polygon(50% 0%, 90% 20%, 100% 60%, 75% 100%, 25% 100%, 0% 60%, 10% 20%);
}
/* Octagon */
tbody:nth-children(8) {
  clip-path: polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%);
}
/* Nonagon */
tbody:nth-children(9) {
  clip-path: polygon(50% 0%, 83% 12%, 100% 43%, 94% 78%, 68% 100%, 32% 100%, 6% 78%, 0% 43%, 17% 12%);
}
/* Decagon */
tbody:nth-children(10) {
  clip-path: polygon(50% 0%, 80% 10%, 100% 35%, 100% 70%, 80% 90%, 50% 100%, 20% 90%, 0% 70%, 0% 35%, 20% 10%);
}

This is not the final solution that I will use in the framework but it demonstrates the use-case of conditional design based on the total number of child elements. To do the same with alternative methods will require much more code with different thinking.

@ramiy
Copy link
Author

ramiy commented Nov 4, 2020

@emilio Another example is to add an axes system to a Polar Chart which is like a Pie Chart but with equal slices. To add radial axes you need to know how many child items you have, and the new :nth-children() pseudo-class will provide the ability to add n axes based on the HTML --> tbody:nth-children(n).

@tabatkins
Copy link
Member

Selecting based on how many siblings an element has is an identical amount of work to :nth-last-child(). (In other words, it's bad, but it's an amount of bad that we've already accepted.) You can tell this is true because :sibling-count() can be desugared into a combination of :nth-child() and :nth-last-child(): :sibling-count(3) can be desugared to :is(:nth-child(1):nth-last-child(3), :nth-child(2):nth-last-child(2), :nth-child(3):nth-last-child(1)) ^_^

Selecting based on how many children an element has is indeed :has()-equivalent, however.

@shuvyA
Copy link

shuvyA commented Nov 5, 2020

Awesome idea!

@Crissov
Copy link
Contributor

Crissov commented Nov 5, 2020

So what would be less complex and not available are:

Not sure any of these has sufficient use cases.

@Loirooriol
Copy link
Contributor

Regarding complexity, we have (from hardest to easiest):

  1. :has()
  2. :has-child(S) = :has(> :is(S)) ([css-selectors] has-child selector #4903)
  3. :nth-children(An+B of S) = :has-child(:nth-child(An+B of S):nth-last-child(1 of S))
    (or maybe = :is(:has-child(:nth-child(An+B of S):nth-last-child(1 of S)), :not(:has-child(S))) if B=0 or A,B≠0, A∣B, A*B<0)

So this one seems the most feasible, and :has() might be unfeasible (for the web, for now). But if :has-child() is feasible, I would prefer it over this one.

@ramiy
Copy link
Author

ramiy commented Nov 8, 2020

@tabatkins you label this as "selectors-5". But @Loirooriol wrote that :nth-children() is the most feasible feature and not complex compare to :has() and :has-child() (#4903) which is labeled as "selectors-4". Can you change this issue label to "selectors-4" ?

@ramiy ramiy changed the title [selectors-4] New selector based on the amount of child elements [selectors] New selector based on the amount of child elements May 29, 2021
@johannesodland
Copy link

johannesodland commented Aug 15, 2022

If/when :has() gets implemented in Firefox it should be possible to style elements based on number of children,
although a little bit cumbersome:

.container:has(:last-child:nth-child(3)) {
   background: green;
}

@bramus
Copy link
Contributor

bramus commented Nov 22, 2022

Extending on @johannesodland’s reply above, I’ve created several selectors leveraging :has(), :nth-child and :last-child to count children:

/* At most 3 (3 or less, excluding 0) children */
ul:has(> :nth-child(-n+3):last-child) {
	outline: 1px solid red;
}

/* At most 3 (3 or less, including 0) children */
ul:not(:has(> :nth-child(3))) {
	outline: 1px solid red;
}

/* Exactly 5 children */
ul:has(> :nth-child(5):last-child) {
	outline: 1px solid blue;
}

/* At least 10 (10 or more) children */
ul:has(> :nth-child(10)) {
	outline: 1px solid green;
}

/* Between 7 and 9 children (boundaries inclusive) */
ul:has(> :nth-child(7)):has(> :nth-child(-n+9):last-child) {
	outline: 1px solid yellow;
}

Post with the details: https://brm.us/css-has-child-count

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

10 participants