Csswg-drafts: [selectors-4] New selector based on the amount of child elements

Created on 4 Nov 2020  ·  16Comments  ·  Source: w3c/csswg-drafts

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

Most helpful comment

In that case it's the same complexity as :has() - i.e., it's complex (see eg https://github.com/w3c/csswg-drafts/issues/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.

All 16 comments

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

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

Related: https://github.com/w3c/csswg-drafts/issues/4559#issuecomment-562374563 proposes sibling-index(), sibling-count(), child-count(), and tree-depth() function values.

I think this can be closed as duplicate of https://github.com/w3c/csswg-drafts/issues/4559

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.

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

In that case it's the same complexity as :has() - i.e., it's complex (see eg https://github.com/w3c/csswg-drafts/issues/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.

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

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

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.

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

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.

Awesome idea!

So what would be less complex and not available are:

  • number of ancestor levels (:root>*>…, mentioned in #4940)
  • number of attributes
  • number of attributes of a specific type
  • number of values for an attribute

    • comma separated

    • whitespace separated

Not sure any of these has sufficient use cases.

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

  1. :has()
  2. :has-child(S) = :has(> :is(S)) (#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.

@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" ?

Was this page helpful?
0 / 5 - 0 ratings