w3c / csswg-drafts

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

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

Open ramiy opened 4 years ago

ramiy commented 4 years ago

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:

noamr commented 4 years ago

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

ramiy commented 4 years ago

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

Loirooriol commented 4 years ago

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

noamr commented 4 years ago

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

ramiy commented 4 years ago

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

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

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.

ramiy commented 4 years ago

: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 commented 4 years ago

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:

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

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

@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 commented 4 years ago

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

Awesome idea!

Crissov commented 4 years ago

So what would be less complex and not available are:

Not sure any of these has sufficient use cases.

Loirooriol commented 4 years ago

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.

ramiy commented 4 years ago

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

johannesodland commented 2 years ago

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 commented 1 year ago

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