37
Tell your users the remaining reading time, in CSS
Note: this demo works only in Chrome 94+
Recently, thanks to the new @scroll-timeline
property available so far only in Chrome 94+, I have seen an interesting demo by @bramus
where the user is aware of the time needed to read a page with a sticky progress bar that stretches horizontally, whose width is dependant on the current scrolling position of the root
element.
I've extended this idea a little bit by coding an animated countdown of the exact time needed to read the page. All you need to do is to just set the time in minutes and the CSS animations will do the work for you.
Try the demo (scroll the page until the end) or jump to the video at the end of the article:
Here is a step-by-step explanation
Basically, two variables --ss
and --mm
hold the values of seconds and minutes and two distinct animations change them. In order to make the interpolation of the values work in the keyframes, these variables have been registered as integers
properties through the Houdini's API integrated on Chrome.
So the --ss
variable is declared in the CSS like this:
@property --ss {
syntax: "<integer>";
initial-value: 0;
inherits: true;
}
while the --mm
is declared instead in a <style>
block, inside the markup because here is where we are going to set the minutes needed (e.g. 7
):
<style>
@property --mm {
syntax: "<integer>";
initial-value: 7;
inherits: true;
}
</style>
Since it's not (yet) possibile to print out the value of a variable using the content
property I've set two counters to print their values through the counter()
function (with a leading 0
when necessary)
counter-reset: ss var(--ss);
...
content: counter(ss, decimal-leading-zero);
and
counter-reset: mm var(--mm);
...
content: counter(mm, decimal-leading-zero);
I've set the scroll-timeline animation in this way
@scroll-timeline scroll {
time-range: 60s;
start: 200px;
end : calc(100% - 150px);
}
The time-range
has been set to 60
seconds in order to make the calculations of the animations easier (at least for my brain) .
To give a meaningful example let's say that we want to start the countdown after 200px
of scroll — to exclude the title or maybe a hero image, but you could even set it to 0
or to another value of course — and we want to also stop it before a hypothetical footer 150px
tall.
This is the most intriguing part due to the tricky computations. Here is the animation of the minutes
@keyframes mins {
to { --mm: 0; }
}
...
animation: mins
/* duration */ 60s
/* timing */ steps(var(--mm), start)
/* delay */ calc(calc(60s / (calc(var(--mm)) - 1)) / 60 / var(--mm))
/* repetition */ 1
/* fill mode */ forwards;
The @keyframes
start from the value we have previously set and should decrease until 0
, so the definition of the last keyframe is enough.
The animation should takes 60s
which is the same amount of time we defined for the @scroll-timeline
. The delay
expression is calculated so that the value of the minutes decreases immediately after 1s
of reading time (e.g. switching from 7:00
to 6:59
), which is good to point out, it is not one second of animation (we will discuss about this in the next section).
The animation of seconds is quite similar but in this case we need to specify the starting value
@keyframes secs {
from { --ss: 59; }
to { --ss: 0; }
}
...
animation: secs
/* duration */ calc(60s / calc(var(--mm)))
/* timing */ linear
/* delay */ calc(calc(60s / (calc(var(--mm)) - 1)) / 60 / var(--mm))
/* repetition */ var(--mm)
/* fill-mode */ forwards;
The only relevant difference here is the duration: seconds need to be updated faster than minutes, running --mm
times from 59
to 0
with a repetition
of --mm
. Here the purpose of the delay
is just to wait the first update of the minutes.
I know that the value of the delay could look odd at first glance:
calc(calc(60s / (calc(var(--mm)) - 1)) / 60 / var(--mm))
but the reason is quite simple: the full animation runs in 60s
and we need to wait 1s
of reading time.
- 1 minute of reading time is mapped to
60s / (--mm - 1)
of the animation (because the value of minutes changes immediately by -1); - 1 second of reading time is
1/60
of the value we have just obtained, so we divide it by60
and we get60s / (--mm - 1) / 60
; - Finally that delay should be taken into account just once and not at every loop of seconds, so we need to further divide this value by the number of cycles, which is exactly the value in minutes so the final expression we need is
60s / (--mm - 1) / 60 / --mm
.
Hope it makes sense (and, in any case, if it works you don't need to touch it) :)
The update of the seconds is amazing but it can be annoying for users who don't like continuous animations and that prefer a reduced motion.
We may use a simple mediaquery:
@media (prefers-reduced-motion: reduce) {
.countdown__display--ss {
display: none;
}
}
and hide the element with the seconds.
“Ta-da!”
— Wall-E
Feel free to follow me on Codepen or Twitter where I usually talk about frontend and trees. Furthemore some days ago I wrote a related article about @scroll-timeline
:
Note: This is a carbon free article so I pledge to plant 25 trees with Ecologi and I'll plant one extra tree for each retweet of the announcement below👇 until Jul 31th, 2021.
New article published on dev.to
dev.to/fcalderan/tell…
«Tell your users the remaining reading time, in #CSS
This is #carbonfree article: I'll plant 1 tree with @Ecologi_hq for each retweet (see the article for the notes)09:16 AM - 19 Jul 2021
(up to 75 trees, retweets from human accounts with 20+ followers)
37