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

[css-values] Proposal: add sibling-count() and sibling-index() #4559

Open
argyleink opened this issue Dec 4, 2019 · 24 comments
Open

[css-values] Proposal: add sibling-count() and sibling-index() #4559

argyleink opened this issue Dec 4, 2019 · 24 comments

Comments

@argyleink
Copy link
Contributor

@argyleink argyleink commented Dec 4, 2019

Problem

Currently we can query a child based on it's child index position or query on children length (with some complex syntax), but we can't use the index or length as values in our styles. Feels like they're known, but not accessible.

so we end up doing stuff like this:

.el {
  --delay: calc(var(--i, 1) * 400ms);
  animation: fadeIn 1000ms var(--delay) forwards;
  width: 100px;
  height: 100px;
  background: darkorchid;
  margin: 20px;
  opacity: 0;
}

.el:nth-child(2) {
  --i: 2;
}

.el:nth-child(3) {
  --i: 3;
}

@keyframes fadeIn {
  100% { opacity: 1; }
}

above source from Codepen


Proposal

2 new functions: sibling-count() and sibling-index(). These should report values based on element count, not node count.

sibling-count()

This function returns the total length of sibling elements as a number, similar to node.childElementCount (docs) or node.children.length but accessible from a sibling. Consider this like a child asking "how many siblings they have".

example usage

Dynamic background color that changes based on total children in the list

ul > li {
  background-color: hsl(sibling-count() 50% 50%);
}

sibling-index()

This function returns the contextual child index as a number. Similar to the value queried with nth-child, this would be a get() call for the contextual child's index in the tree. Consider this like a child asking "what position am I in this family".

example usage

Dynamic background color that changes based on child index position, resulting in a stepped gradient effect

ul > li {
  background-color: hsl(calc(sibling-index() * 10) 50% 50%);
}

All Together

Example code is a slightly modified version of Jake Archibald's comment here 👍

ul > li {
  /* Stagger from start to end */
  animation-delay: calc(sibling-index() * 100ms);

  /* Stagger from end to start */
  animation-delay: calc(sibling-count() - sibling-index() * 100ms);
}



Use Cases

  • staggered animations: example1 example2
  • dynamic & contextual color systems
  • dynamic & contextual spacing
  • dynamic & contextual distance
  • etc

splitting


Conclusion

Most other proposals for similar functionality, request access to counters() as a unit that can be passed to calc(). But I feel that is overloading the counters feature and is rooted in a mindset of leveraging something "close" to what is needed, where what's actually wanted is the child index position for visually reasonable and meaningful UI feedback and presentation.

By proposing a solution that doesn't involve counters, I hope to bypass much of the pain points associated with the feature to help unblock the large and ever-growing set of use cases that could leverage these contextual values.

Sources & Chatter

#1869
#1176
#1026
https://www.w3.org/TR/selectors-4/#child-index
https://twitter.com/shshaw/status/1201978228375724032?s=20
https://twitter.com/smfr/status/1202276694230306816?s=20 @smfr

@shshaw

This comment has been minimized.

Copy link

@shshaw shshaw commented Dec 4, 2019

Definitely on board for this. These functions would pave the way for making Splitting.js obsolete and enable some really great animations and effects.

Small point for discussion: Should sibling-count() be the count of all the parent's children OR the count of this element's siblings (practically parent.children.length - 1)? Because "semantically" if you're asking for the count of siblings, it would not include the element itself. In plain language: "I have one brother" versus "My parents have two sons"

@jakearchibald

This comment has been minimized.

Copy link
Contributor

@jakearchibald jakearchibald commented Dec 4, 2019

Proposal looks good, although it'd be nice if an element could know it's children count too.

@jakearchibald

This comment has been minimized.

Copy link
Contributor

@jakearchibald jakearchibald commented Dec 4, 2019

I'm not sure why we have two issues though. Feels like one of them should be closed.

@smfr

This comment has been minimized.

Copy link
Contributor

@smfr smfr commented Dec 4, 2019

Maybe there should also be a depth query (with some consideration for what happens in shadow trees).

@AmeliaBR

This comment has been minimized.

Copy link
Contributor

@AmeliaBR AmeliaBR commented Dec 4, 2019

This looks like a straight duplicate of #1869, can we close this & copy the discussion over there?

That conversation discusses the counter-value() as one possible solution, but isn't restricted to it.

@argyleink

This comment has been minimized.

Copy link
Contributor Author

@argyleink argyleink commented Dec 4, 2019

Tab and I felt a new one was due since there were a few issues that alluded to similar functionality but generally had counters() involved or weren't as minimal / specific as this. That's why the issues are linked that helped lead to this one. BUT, I'm ok moving it over to another thread if we want!

@emilio

This comment has been minimized.

Copy link
Collaborator

@emilio emilio commented Dec 4, 2019

Why a function?

(With an implementor hat on) it's a bit unfortunate having to do the element instead of node count, as the first is O(N) but the second is O(1)..

@argyleink

This comment has been minimized.

Copy link
Contributor Author

@argyleink argyleink commented Dec 4, 2019

What could we use that's not a function @emilio!?

Element vs Node, browser internals don't do any distinguishing that you can piggyback on? 0(N), even if the children are fixed and there's no recursion required? tell me more, this sounds interesting 🙂

@emilio

This comment has been minimized.

Copy link
Collaborator

@emilio emilio commented Dec 4, 2019

Well, I was thinking there's nothing really preventing you from using an ident or such (functions without arguments are not great), but I guess that would make some properties that can take <ident> and <number> or <integer> ambiguous. Also I guess I could see sibling-count(<selector>) or something being a thing...

Element vs Node, browser internals don't do any distinguishing that you can piggyback on? 0(N), even if the children are fixed and there's no recursion required? tell me more, this sounds interesting slightly_smiling_face

At least Gecko keeps the node children count in the parent node (here), so getting the sibling node count is just parentNode.childCount - 1 which is pretty fast. For elements, we'd have to actually traverse all children and check they're elements.

There's nothing preventing us from tracking element count either, but it grows every node (or at least container node) which is not great.

But it seems other engines don't do the same optimization as Gecko, so feel free to take that point as an implementation detail...

@Crissov

This comment has been minimized.

Copy link
Contributor

@Crissov Crissov commented Dec 5, 2019

This feels like it should be solved either with predefined counters or with predefined variables that do not have a double-hyphen prefix.

  • var(n) or var(child) or var(child-n) for the index value used for :nth-child(), which is calc(var(preceding-siblings) + 1), i. e. 1 for :first-child and for :only-child, where var(siblings) = 0
  • var(m) or var(last-child) or var(-child) or var(child-m) for the index value used for :nth-last-child(), which is calc(var(succeeding-siblings) + 1) or calc(var(siblings) - var(child) + 2), i. e. 1 for :last-child and for :only-child, where var(siblings) = 0 and var(child) = 1
  • var(i) or var(type) or var(type-n) for the index value used for :nth-of-type()
  • var(j) or var(last-type) or var(-type) or var(type-m) for the index value used for :nth-last-of-type()

https://drafts.csswg.org/selectors-4/#child-index

@faceless2

This comment has been minimized.

Copy link

@faceless2 faceless2 commented Dec 5, 2019

I'd been trying to work up a coherent way to do this using counter() and target-counter() for some time, but I've (somewhat grudgingly) come to the conclusion that a new function is a better option - it avoids all sorts of hazards inherent in counters, which are considerably more complex then they first appear (when it comes to scoping).

I would personally lean towards a new function over a syntax that was a) not a function or b) used var(), because it leaves more room for expansion in the future: it's easy enough to define the sibling-index() function now, with no arguments, and get that implemented. If users demand more in the future, you can supply arguments (e.g. a selector as mentioned by @emilio). You would struggle to defined that cleanly with a var().

Can I suggest that instead of sibling-index() and sibling-count() we consider index(sibling) and count(sibling) - because you can guarantee there will be situations where you want the count of your children rather than siblings, for example to solve the issue from #4211:

ol[reversed] {
    counter-reset: list-item count(children);
}

It would be good to leave that option available in the syntax, even if it's not implemented yet.

(edit: see also #4181)

Idle musing on other uses:

.clockhand {
    position: absolute;
    left: calc(50% + sin(index(sibling) / count(sibling)) * var(--radius));
    top: calc(50% + cos(index(sibling) / count(sibling)) * var(--radius));
}
@jakearchibald

This comment has been minimized.

Copy link
Contributor

@jakearchibald jakearchibald commented Dec 5, 2019

In #1869 (comment) I showed that if you can get a children count, you can use that to get a sibling count, so if we need an minimum viable product, then I'd rather have a children count than sibling count.

I also think "sibling count" is misleading. For example, I am one of two children, but doesn't that mean I have one sibling? Whereas here we're using "sibling count" to mean "sibling count + myself".

@faceless2

This comment has been minimized.

Copy link

@faceless2 faceless2 commented Dec 5, 2019

How about index(element) and/or index(node)? It covers the point made by emilio above.

@jakearchibald

This comment has been minimized.

Copy link
Contributor

@jakearchibald jakearchibald commented Dec 5, 2019

I don't mind too much between index(element) vs index-element().

CSS doesn't really allow you to interact with non-element nodes so adding something like index(node) would be pretty new/unusual.

@shshaw

This comment has been minimized.

Copy link

@shshaw shshaw commented Dec 5, 2019

@jakearchibald

This comment has been minimized.

Copy link
Contributor

@jakearchibald jakearchibald commented Dec 5, 2019

count(siblings) for getting parent’s element count minus 1.

Does anyone have a use-case where "parent's element count minus 1" is the number you actually want, rather than "parent's element count"?

@shshaw

This comment has been minimized.

Copy link

@shshaw shshaw commented Dec 5, 2019

@jakearchibald child count - 1 is a value I use in Splitting demos frequently for getting a “percentage” like decimal (range from 0 to 1 of the child’s index relative to the total). Here's a quick example from the initial sibling-count() discussion where I use that "child count - 1" value to stagger an animation-delay over the course of 2s.

animation-delay: calc( 2s * ( var(--sibling-index) / var(--sibling-count) ) );

https://codepen.io/shshaw/pen/LYEEKMQ

This can of course be achieved by doing child-count() - 1, but if we're calling it the count of the siblings then it seems odd that it would include the element itself. I do find the - 1 value very useful in my instances, but I'm not beholden to a - 1 value if there's a strong case against, or if this is just a difference in semantics.

@Loirooriol

This comment has been minimized.

Copy link
Collaborator

@Loirooriol Loirooriol commented Dec 5, 2019

animation-delay: calc( 2s * ( var(--sibling-index) / var(--sibling-count) ) );

This assumes that the index is 0-based. I would prefer it to be 1-based for consistency with nth-child.
Also, be aware that if you only have 1 child, you will have a 0/0 division.
I would argue that index-1-based / parent-children-count would be safer, and not getting 0 for the 1st child doesn't seem a big problem.

@shshaw

This comment has been minimized.

Copy link

@shshaw shshaw commented Dec 5, 2019

@tomhodgins

This comment has been minimized.

Copy link

@tomhodgins tomhodgins commented Dec 5, 2019

I can immediately see the value of both sibling-count() and sibling-index() and other related ideas too; in the past I've applied two similar concepts to styling:

  • index of this tag amongst its siblings (the children of its parentElement)
  • index of this tag amongst the list of tags in the document matching a given selector (similar to index inside querySelectorAll results)

For the second idea I've made a plugin to help in the past, though now when I want to use this concept I usually reach for CSS custom properties and a little JS for a cleaner approach. Having the awareness of a tag's index inside parentElement.children or document.querySelectorAll(selector) in CSS natively, as well as the parentElement.children.length and/or el.children.length could simplify lots of existing tricky styles that have been built in much more complex ways!

@tabatkins

This comment has been minimized.

Copy link
Member

@tabatkins tabatkins commented Dec 6, 2019

So I agree with the proposed set of four functions: sibling-index(), sibling-count(), child-count(), and tree-depth().

concern about "sibling" naming

I get the concern on precision, but I don't know of another word covering the concept, and "sibling" can be used in this sense (like "I'm one of three siblings"). I think it's the most reasonable name. I do not like being less specific with something like count() or index().

node vs element

Node count is just so, so un-useful. ^_^ But also, having these functions disagree with the :nth-child() selectors would be a terrible idea imo.

function vs keyword

Hm, yeah, could go either way. I don't plan on extending the functions to more stuff, so I guess keywords could work. I'm slightly wary of adding "keywords that can be used anywhere" because of the potential syntax conflicts (such as animation-name in the 'animation' property); functions avoid that. (We should have ensured that author-defined names were syntactically distinguishable in all cases earlier, but that's a legacy mistake.) If these were usable only in calculations (that is, you have to wrap it in a calc() or other math function), my concern would be alleviated.

Tho the suggestion to later extend this to allow a selector, a la the :nth-child(... of <selector>), is a reasonable future extension point, and thus a decent argument for function over keyword.

1-index vs 0-index

Definitely 1-indexed, just like :nth-child(). Diverging from :nth-child() would be a terrible mistake.

count of siblings vs count of "siblings other than me"

I think the concept is a lot cleaner to express and understand if it's just "how many children there are"; both "sibling count" and "child count" should agree. Some cases definitely want "siblings other than me", but that's trivial to do in a calc(), and you're probably already using a calc() anyway. (The given example is, to do a division as well.)

count/index across the entire document

Not planning on doing this; it would be super expensive. Use-cases are small enough, as far as I'm aware, that I'm happy to leave that to JS for decorating the element with a value.

using var() with predefined (not --prefixed) keywords

This would be a misuse of the var() syntax, and have unfortunate implications regarding parsing; we wouldn't be able to reject a color: children-count() even tho the function can only ever return integers.

@Crissov

This comment has been minimized.

Copy link
Contributor

@Crissov Crissov commented Dec 7, 2019

I always expected that var() would eventually be extended to support predefined variables without a double-hyphen prefix (like env()), because otherwise that ugly convention could have been avoided altogether, so I donʼt see why it would be syntax misuse, but I do understand the type-casting implications, which is why predefined counters seem like a more natural fit for inherently integer variables – alas, they have other problems.

Anyway, selectors in property values always seem like a bad idea, so Iʼm against a proactive function syntax without parameters, which probably appears natural only to a programmerʼs brain. In pseudo-classes, there are already precedents of something like :foo paired with :foo(bar) if this soothes those concerns.

@myfonj

This comment has been minimized.

Copy link

@myfonj myfonj commented Jan 6, 2020

Current state of proposed sibling-index / tree-depth functions FMPoV lacks (or fails to clearly show capability of) quite desired features compared to what counter-increment can do already:

  • skip some siblings,
  • fine grain what exactly participates on the resulting value: e.g. take attributes or nesting level into account.

Both are possible using counter-increment and standard selectors, as illustrated.
Counters displayed in content are meant to be available for other CSS functions or custom properties:

/* fig.1: skip hidden <tabs><tab>(1)</tab><tab hidden>(skipped)</tab><tab>(2)</tab></tabs> */
tabs { counter-reset: index; }
tabs > tab:not([hidden])::before { counter-increment: index; content: 'Index: ' counter(index); }

/* fig.2: nesting depth <p><em>(1)<em>(2)<em>(3)</em></em><em>(2)</em></em></p> */
p { counter-reset: depth; }
p em::before { counter-increment: depth; content: 'Depth: ' counter(depth); }
p em::after { counter-increment: depth -1; content: ''; }

I think above use cases are quite relevant for problem in question. Unless current proposal will be expanded so that

  • problem depicted in fig.1 could be solved with something like
    tabs > tab{--index: sibling-index(:not([hidden])); }
    (function argument filters previous siblings),
  • and problem from fig.2 with something like
    p em{--depth: tree-depth(:has(> em)); }
    (function argument filters parents chain),

(yes, using selectors in properties feels super weird), or any other way, I'd rather lean towards #1026.


Please pardon unasked intervention and errors; I don't follow all conversations around here, so this had very likely been discussed before; I just felt it should be mentioned here.

@Loirooriol

This comment has been minimized.

Copy link
Collaborator

@Loirooriol Loirooriol commented Jan 6, 2020

@myfonj While counters can be more flexible, they have some complex inheritance. In order to know the value of a counter in an element, you may have to iterate all the descendants of the previous siblings of the element. And this can be bad for parallelization, see #1026 (comment). So it seems less doable to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.