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] enable the use of counter() inside calc() #1026

Open
psylok opened this issue Feb 11, 2017 · 41 comments
Open

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

psylok opened this issue Feb 11, 2017 · 41 comments

Comments

@psylok
Copy link

psylok commented Feb 11, 2017

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
Copy link
Contributor

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
Copy link

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;
    }
}

@fantasai fantasai added css-values-4 Current Work css-lists-3 Current Work labels Mar 30, 2017
@fantasai fantasai changed the title [css-functions] enable the use of counter() inside calc() [css-values] enable the use of counter() inside calc() Mar 30, 2017
@Loirooriol
Copy link
Contributor

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
Copy link
Contributor

Crissov commented Oct 16, 2017

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

@tabatkins
Copy link
Member

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
Copy link
Member

upsuper commented Nov 2, 2017

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
Copy link
Contributor

@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 #1869 (comment); 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
Copy link
Member

upsuper commented Nov 3, 2017

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
Copy link
Member

upsuper commented Nov 3, 2017

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
Copy link
Member

kizu commented Jan 15, 2018

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
Copy link
Member

Two more use cases:

  • Staggered animations by using it in animation-delay
  • Reversing the z-index order by using it in z-index

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
Copy link

jonjohnjohnson commented Apr 29, 2018

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
Copy link

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
Copy link
Member

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?

@Loirooriol
Copy link
Contributor

Loirooriol commented Feb 3, 2022

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

@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
Copy link

JJanz commented Feb 3, 2022

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
Copy link

alystair commented Sep 15, 2022

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:

  • much more succinct, less verbose
  • easier to revise and maintain
  • less fragile
@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
Copy link

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
Copy link

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
Copy link

mnik01 commented Jun 2, 2023

👍
+1 on this! Very hot

@LeaVerou
Copy link
Member

LeaVerou commented Jun 2, 2023

👍 +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
Copy link

brandonmcconnell commented Jun 18, 2023

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);
}

@rthrejheytjyrtj545
Copy link

rthrejheytjyrtj545 commented Jun 18, 2023

@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
Copy link

@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
Copy link
Contributor

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
Copy link

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

@tavin
Copy link

tavin commented Jul 11, 2023

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
Copy link
Contributor

@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
Copy link

tavin commented Jul 12, 2023

@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
Copy link
Contributor

Doesn't seem much likely to happen due to #1026 (comment)

@kizu
Copy link
Member

kizu commented Jul 12, 2023

Doesn't seem much likely to happen due to #1026 (comment)

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
Copy link

Alternatively maybe some sort of nth-usage inside calc?

foo:nth-child(n) {
  animation-delay: calc(n) * 20ms; 
}

@Loirooriol
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
No open projects
Development

No branches or pull requests