w3c / csswg-drafts

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

[css-values] enable the use of counter() inside calc() #1026

Open psylok opened 7 years ago

psylok commented 7 years ago

Feature request. It would be nice to be able to use the counter() function inside of calc() function. That would enable new possibilities on layouts.

I copy here the link to a thread which proposed it last august. https://lists.w3.org/Archives/Public/www-style/2016Aug/0073.html

AmeliaBR commented 7 years ago

Relevant section of the spec, which includes an inline issue discussing possible use cases. But getting an issue on GitHub will hopefully provide an easier forum for other people to mention their use cases and/or possible implementation issues.

MelSumner commented 7 years ago

Hi, ran into a case today where this would have helped, so posting a comment here.

I'm setting up some CSS for an image/logo slider. While I know right now that there are 8 images, I don't know how many there will be in the future (nor do I wish to have to know). I'm using only CSS to animate it, and it would be useful to be able to say "for each one of the items that exist (, increment this property by x." I ended up using sass to do something like this:


@for $i from 1 through 8 {  
    &:nth-of-type(#{$i}) {
        $delay: calc(5s * #{$i});
    animation: slideIn 45s linear $delay infinite;
    }
}
Loirooriol commented 7 years ago

The problem with this proposal is that counter() returns a <string>, but calc() does not work with strings. For example, counter(id, upper-latin) might be 'A'. How exactly is calc() supposed to know that this means 1?

So I think we need some way to get the numeric value of a counter, either by adding some parameter to counter() or a new function. This should be allowed to appear anywhere an integer is expected, including (but not necessarily in) calc(). I wrote my thoughts in #1871.

Crissov commented 7 years ago

JFTR, I would hate to see int() or str2num() in CSS.

tabatkins commented 7 years ago

Yeah, an explicit parsing function is probably bad. No real need for it. But having something that can retrieve a counter value as a number (rather than going to the extra effort of formatting it as a string) would work. (Same as how attr() has functionality to parse the attr value as a number or dimension.)

upsuper commented 7 years ago

Wouldn't allowing counter() in calc() either ruin any attempt to parallelize styling, or make us need to delay calculating of every numeric value to used-value time?

Loirooriol commented 7 years ago

@upsuper Fair point, counter values are inherited from the immediately preceding element in document order instead of from the parent element as usual. So maybe it would be better to add a sibling-index() function as Tab Atkins proposed in https://github.com/w3c/csswg-drafts/issues/1869#issuecomment-340078522; it would be less powerful but I think it would cover most usecases.

However, note that the CSS Lists draft currently allows counter() anywhere a <string> is expected, doesn't this have the same problem? If it's possible to use a counter value as a string, it should also be possible to use it as an integer.

upsuper commented 7 years ago

However, note that the CSS Lists draft currently allows counter() anywhere a <string> is expected, doesn't this have the same problem? If it's possible to use a counter value as a string, it should also be possible to use it as an integer.

Firstly, there are a lot fewer places where <string> is accepted, and they are usually relatively less complicated than <length> and its friends when it comes to layout, so expanding them in used-value time (like what is currently done for content property) may not be as bad.

Also, allowing counter() anywhere <string> is allowed is something new. counter() functions are listed as independent item of content property in CSS2. And I'm not aware of any browser who has implemented that anywhere outside content. That means, its feasibility may also be questioned, and my argument above can potentially be apply to that as well.

upsuper commented 7 years ago

Firstly, there are a lot fewer places where <string> is accepted, and they are usually relatively less complicated than <length> and its friends when it comes to layout, so expanding them in used-value time (like what is currently done for content property) may not be as bad.

It may not be as bad, but it could still be very bad. One issue comes to my mind is that <family-name> can be a <string>, and font-family is inherited, so you may really want to make counter() be expanded in computed-value time (rather than in used-value time like what we do now for content), otherwise it can lead to some funny behavior. This is some case which seems to be neither useful nor easy to implement, which would be a native consequence if we allow counter() be in anywhere <string> is allowed.

I guess we should start this topic in a separate issue, now.

kizu commented 6 years ago

I would really, really love to have counter's value available inside calc(). The syntax I'd propose could be something like counter-value() that would return the counter's value as an integer. Other than the use-cases for sibling-index, this could be used for a lot more cases (not only experimental ones). While I understand that that can be non-trivial to implement, the possibilities it would unlock would be tremendous.

LeaVerou commented 6 years ago

Two more use cases:

Regarding syntax, I like @tabatkins's proposal for a new formatting argument, if an explicit syntax is required. Although IMO ideally it would be nice if we could auto cast number-like strings to numbers in calc() akin to how many programming languages do it (including JS). It's not like strings actually do anything in calc(), so there's no disambiguation problem.

jonjohnjohnson commented 6 years ago

My sad excuse of a utility without this feature...

...
.count--3 { --count: 3 }
.count--4 { --count: 4 }
.count--5 { --count: 5 }
...
.count > :nth-child(1) { --count-current: 1 }
.count > :nth-child(2) { --count-current: 2 }
.count > :nth-child(3) { --count-current: 3 }
...
.el {
  animation-delay: calc((var(--count, 0) - var(--count-current, 0)) * 0.1s);
} 
faceless2 commented 4 years ago

I like this in general and I'd love to see it implemented too. But I'd really, really hate implementing it.

All of the paged media layout engines have the same problem with counters, in particular "page" and "pages", although technically it's any counters incremented in the @page margin areas (still a largely theoretical constraint at the time of writing). The moment you hit one of these you have to consider a second layout pass - it's not always required, but for something like span::after { content: counter(pages upper-roman); } you really haven't got much choice.

You need the first layout pass to count the pages, or to work out with certainty which page your element is on (that's more of an issue with target-counter(nnn, page) to be fair). But once you start introducing counters to other properties - say margin-top: calc(counter(pages) * 20px) - you've introduced a loop: layout depends on values computed from an earlier pass of the layout, and so on.

It's not quite as awful for the "page" counter, or any counter other than "pages". But it's still a little complex: orphans: calc(counter(page) * 20) may force a page-break, which would change the value of orphans...

So although I'm not exactly against this, I just wanted to flag that it is almost unfeasibly hairy for some cases. Think of the multiple passes required to stabilise layout for ::first-line, except it's a whole document that needs to be stabilised. These would need to be considered if this goes anywhere.

LeaVerou commented 4 years ago

Won’t we get this for free once we have a function to convert between types, the likes of which has been discussed multiple times?

steren commented 3 years ago

I landed here because I assumed I would be able to do something like this in CSS:

li:nth-child(n) {
  background-color: hsl(calc(10 *n), 100%, 70%);
}
yinonov commented 3 years ago

working on a calendar like grid, was sure i'd be able to layout overlapping scheduled events by the use of counter-value. js got tons of practices for a conditional index. can't css have 1? (excluding nth-child selectors🤓)

ShahriarKh commented 2 years ago

Another use case here:

.item:nth-of-type(n) {
  transform: rotate(calc(n*10deg)); /* 10 comes from 360/totalItemsCount */
}
LeaVerou commented 2 years ago

Another use case here:

.item:nth-of-type(n) {
  transform: rotate(calc(n*10deg)); /* 10 comes from 360/totalItemsCount */
}

Could you elaborate a bit on this? n cannot be used in calc(), and it's unclear what it would mean if it could. :nth-of-type(n) matches everything, so you could remove that pseudo-class entirely with the same result. If what you need access to is the total count of items, counter-value() could not help here, as it would be different on each item. Could you also elaborate on why you are dividing 360deg by the total number of items? Are you placing them around a circle of something? A screenshot of a sample rendering would help a lot here!

Loirooriol commented 2 years ago

@ShahriarKh Your use case seems better covered by #4559:

.item { transform: rotate(calc(1turn * sibling-index() / sibling-count())) }

With a counter-based approach you would also need #3667 to know the totalItemsCount.

ShahriarKh commented 2 years ago

@LeaVerou

Could you elaborate a bit on this? n cannot be used in calc(), and it's unclear ....

Yeah I know this isn't possible. I find this discussion when I was playing with CSS: https://codepen.io/shahriarkh/pen/rNGwQMQ I wanted to place some bars inside the circle like this:

drawing

Declaring rotation for every bar isn't a good idea, so I wondered it will be useful if we had something like this:

.bar:nth-of-type(n) {  
    transform: rotate(calc(n * 36deg))
}
DesignThinkerer commented 2 years ago

Here's another way this feature could be used : decreasing the size of siblings.

.circles { 
  counter-reset: n;
 }

.circles .circle {
counter-increment: n;
width:calc(100% / counter(n)  );
}

Is there any hope to get something like this in CSS one day or is that a task more suited for javascript ?

JJanz commented 2 years ago

And I got here to sadly find out this is still open and with yet another use case (I couldn't find it by scanning the ones here): counting DOM depth, which I intented use to indent only one element in nested tags. Like:

body {
    counter-reset: tag-depth;
}
* > .indented {
    counter-increment: tag-depth;
}
.indented {
    margin-left: calc(1.5rem * counter-value(tag-depth);
}

I would totally solve this with recursive adding 1 to a custom property (like a * > .indented { --tag-depth: calc(var(--tag-depth) + 1);}) but sadly this isn't allowed either. =(

Loirooriol commented 2 years ago

@JJanz Rather than counters, you may have better luck with inherit(), already approved by CSSWG resolution: https://github.com/w3c/csswg-drafts/issues/2864#issuecomment-816280875

@property --tag-depth {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}
.indented {
  --tag-depth: calc(1 + inherit(--tag-depth));
  margin-left: calc(1.5rem * var(--tag-depth);
}

Note that with counters aren't probably what you want for depth, e.g. the 2nd .indented would have a counter value of 2:

<div class="indented"></div>
<div class="indented"></div>
JJanz commented 2 years ago

Rather than counters, you may have better luck with inherit(), already approved by CSSWG resolution: #2864 (comment)

Neat! I couldn't get to it in my research! Thanks!

Note that with counters aren't probably what you want for depth

Yes, I was aware my example could fail somehow (I did see it fail by a quick test writing counter(tag-depth) on content property but thought maybe there was a way a through - and I was out of options, anyway), as in fact I made it from an actual code I started in the lines of the second example I gave. First made it when sure it would work and on a train of thought that is exactly what inherit() seems to do but, on no avail, following research brought me to counters and here.

Again, thank you so much for your help! =)

alystair commented 2 years ago

A major styling issue front-end developers and designers experience is when they are tasked with dynamically changing styling of repeated elements. As of writing this is normally done either via swaths of :nth-child() and/or Javascript.

Permitting the use of counters within calc() can greatly simplify and condense such verbose code.

Let's say we wanted to animate each of the following list items in a cascade.

<ol>
    <li>Foo</li>
    <li>Bar</li>
    <li>Bizz</li>
    <li>Buzz</li>
</ol>

Currently it has to be manually like so:

@keyframes reveal { 0% { opacity:0; } }
ol>* { animation: reveal both 3s; }
li:nth-child(1) { animation-delay: 0.5s; }
li:nth-child(2) { animation-delay: 1s; }
li:nth-child(3) { animation-delay: 1.5s; }
li:nth-child(4) { animation-delay: 2s; }

When the number of items or timing changes, each element must be revised.

However, counter values allow code that is:

@keyframes reveal { 0% { opacity:0; } }
ol>* {
    animation: reveal both 3s;
    animation-delay:calc( counter-value(list-item) / 0.5s );
}

This opens up the world to so many other effects that were limited to JS and annoying manual composition in a clean form... although my creative juices are running low - I bet someone more creative such as @argyleink can come up with many other fun uses.

Perhaps limiting use to the special list-item counter to start with could guarantee some sort of baseline to get the rest sorted? My concern as an end user is that other potential methods (Such as the @property example shown in a comment above) don't have the clarity that a counter value, or the proposed sibling-index() have...

//EDIT// Just realized I commented on the sibling-index() proposal last year, whoops. Hope it gains more traction.

john-hascall commented 2 years ago

I too was sad that this did not work. I had to turn something elegant:

.nav ol li.active ~ .indicator {
  transform: translateX(calc(70px * calc(counter(list-item) - 1)));
}

into a whole string of hideous and fragile:

.nav ol li:nth-child(0).active ~ .indicator {
  transform: translateX(calc(70px * 0));
}
    ... cases 1..11 here ...

.nav ol li:nth-child(12).active ~ .indicator {
  transform: translateX(calc(70px * 12));
}
sirisian commented 1 year ago

If you need another example, this can be useful with overlapping grid items. In this example the user wants item1 to span both columns and item2 to be in the second column for each row. Doing this without counters requires explicit row numbers.

#grid {
  display: grid;
  grid-template-columns: repeat(2, 20px);
  counter-reset: row;
}
.item1 {
  counter-increment: row;
  grid-column: 1 / span 2;
  grid-row: counter(row);
}
.item2 {
  grid-column: 2;
  grid-row: counter(row);
}
<div id="grid">
  <div class="item1">1</div>
  <div class="item2">1</div>
  <div class="item1">2</div>
  <div class="item2">2</div>
</div>
mnik01 commented 1 year ago

👍 +1 on this! Very hot

LeaVerou commented 1 year ago

👍 +1 on this! Very hot

Hi @mnik01 and welcome to the csswg repo! Just a quick note that since these discussions can get quite long, we tend to avoid posting +1s etc unless we also have something else to say in addition to the expression of support and use reactions instead to simply express support.

brandonmcconnell commented 1 year ago

Hey all, as the scope of this issue has changed quite a bit from its original concept/spec, it may be worth considering changing the name on the ticket, so it can be found more easily.

I essentially duplicated the more recent evolution of this issue in #8981.


@tabatkins made a good point in my related ticket that using n to reference the n from an nth-child statement wouldn't work well since multiple nth-child statements can be used on a single selector.

One possibility would be to introduce a second optional argument to nth-child for a CSS custom property to assign the iterator value to, like this:

element:nth-child(n + 1, --n) {
  transition: /* ... */;
  --delay: calc(var(--n) * 200ms);
  transition-delay: var(--delay);
}

In either case, we should still discuss all the pros/cons/implications of allowing selectors to also be property-declarative in this way, as this would set a very new precedent not before used in CSS afaik.

Another consideration of using a second argument in those pseudo-selector expressions for this is that it would also beg the question, "Would each :nth-child() second arg value yield a unique value per its expression, or would this iterator value match the nth matched element?"

element:nth-child(n, --m):nth-child(n + 1, --n) {
  /* are `m` and `n` equal?
     it's a bit unclear. */
}

Alternatively, should an iterator require a completely different syntax altogether, such as the following example, where the custom property name is used completely outside the expression itself, after the selector and before the style block?

element:nth-child(n):nth-child(n + 1) --n {
  transition: /* ... */;
  --delay: calc(var(--n) * 200ms);
  transition-delay: var(--delay);
}
bleper commented 1 year ago

@brandonmcconnell, custom properties are intentionally left untouched by the language, so if provide this constant as a variable, then it should be done at the property declaration level: for example, some special value or function nth for the initial value descriptor.

brandonmcconnell commented 1 year ago

@rthrejheytjyrtj545 That makes sense. In that case, it would probably make the most sense as a <calc-constant> that needs to be used in a property (standard or custom) with the styles under a selector with an :nth-child() rule, or similar, which would match the nth matched element.

Loirooriol commented 1 year ago

Counters can be modified within all the subtree in abitrary ways. If you just want the element index and sibling count, refer to #4559 instead.

brandonmcconnell commented 1 year ago

@Loirooriol Thanks for pointing me to that issue. Adding my thoughts there. 🙂

tavin commented 1 year ago

It's not possible for counter() to return a string or integer depending on whether it's inside a string expression or calc() expression? I just mean, why counter-value()?

Background: I wanted to initialize a counter using the counter() of another counter...

Related: Has there been discussion anywhere about permitting @property { inheritance: tree-order; } or similar? It would be another way to propagate a value per the same rule that counters follow.

Loirooriol commented 1 year ago

@tavin calc(counter(foo, lower-alpha)) makes no sense. Also it's confusing if the same expression can resolve to entirely different types depending on the context.

tavin commented 1 year ago

@Loirooriol we can debate whether counter(foo) is confusing in a numeric expression, but any idea whether this or even calc(counter-value(foo)) has a real chance of being adopted?

Loirooriol commented 1 year ago

Doesn't seem much likely to happen due to https://github.com/w3c/csswg-drafts/issues/1026#issuecomment-341295647

kizu commented 1 year ago

Doesn't seem much likely to happen due to https://github.com/w3c/csswg-drafts/issues/1026#issuecomment-341295647

Could something akin to setting up a particular scope for a certain counter help with that?

Similar to how we can use timeline-scope to set up a scope for an animation? So something like a counter-value-scope: foo on a container switching anything that tries to access this particular counter's value inside to this non-parallel/delayed slower mode? This way nothing would be affected by default, and authors couldn't easily “break” the performance of everything, as they would be required to explicitly state which counter should receive this behavior.

tomasdev commented 1 year ago

Alternatively maybe some sort of nth-usage inside calc?

foo:nth-child(n) {
  animation-delay: calc(n) * 20ms; 
}
Loirooriol commented 1 year ago

@tomasdev See #8981, "We cannot make values dependent on what kind of selector they were applied with". However, in this case you could use animation-delay: calc(sibling-index() * 20ms); which is already in the spec.

alystair commented 1 week ago

@tomasdev See #8981, "We cannot make values dependent on what kind of selector they were applied with". However, in this case you could use animation-delay: calc(sibling-index() * 20ms); which is already in the spec.

Thanks for the update re: sibling-index() @Loirooriol! This would permit me to achieve my intended use showcased in my 2022 comment above. Hope to see it implemented in the future. When used along with sibling-count() it could generate a sort of seed based randomness for artistic use (confetti, etc)... although I hope it works on #foo::before{content:''} elements!

Loirooriol commented 1 week ago

@alystair For randomness, see https://drafts.csswg.org/css-values-5/#randomness

For sibling-*() on pseudo-elements, see #9573

LeaVerou commented 1 week ago

sibling-index() is nice but counter() is more powerful, as it doesn't just count siblings (and can be incremented with any increment). I would much rather see something that works like counter() but produces a number.