Jump to content

Welcome to CodeNameJessica

Welcome to CodeNameJessica!

💻 Where tech meets community.

Hello, Guest! 👋
You're just a few clicks away from joining an exclusive space for tech enthusiasts, problem-solvers, and lifelong learners like you.

🔐 Why Join?
By becoming a member of CodeNameJessica, you’ll get access to:
In-depth discussions on Linux, Security, Server Administration, Programming, and more
Exclusive resources, tools, and scripts for IT professionals
A supportive community of like-minded individuals to share ideas, solve problems, and learn together
Project showcases, guides, and tutorials from our members
Personalized profiles and direct messaging to collaborate with other techies

🌐 Sign Up Now and Unlock Full Access!
As a guest, you're seeing just a glimpse of what we offer. Don't miss out on the complete experience! Create a free account today and start exploring everything CodeNameJessica has to offer.

A CSS-Only Star Rating Component and More! (Part 2)

(0 reviews)
by: Temani Afif
Fri, 07 Mar 2025 13:14:12 +0000


In the last article, we created a CSS-only star rating component using the CSS mask and border-image properties, as well as the newly enhanced attr() function. We ended with CSS code that we can easily adjust to create component variations, including a heart rating and volume control.

This second article will study a different approach that gives us more flexibility. Instead of the border-image trick we used in the first article, we will rely on scroll-driven animations!

Here is the same star rating component with the new implementation. And since we’re treading in experimental territory, you’ll want to view this in Chrome 115+ while we wait for Safari and Firefox support:

Do you spot the difference between this and the final demo in the first article? This time, I am updating the color of the stars based on how many of them are selected — something we cannot do using the border-image trick!

I highly recommend you read the first article before jumping into this second part if you missed it, as I will be referring to concepts and techniques that we explored over there.

One more time: At the time of writing, only Chrome 115+ and Edge 115+ fully support the features we will be using in this article, so please use either one of those as you follow along.

Why scroll-driven animations?

You might be wondering why we’re talking about scroll-driven animation when there’s nothing to scroll to in the star rating component. Scrolling? Animation? But we have nothing to scroll or animate! It’s even more confusing when you read the MDN explainer for scroll-driven animations:

It allows you to animate property values based on a progression along a scroll-based timeline instead of the default time-based document timeline. This means that you can animate an element by scrolling a scrollable element, rather than just by the passing of time.

But if you keep reading you will see that we have two types of scroll-based timelines: scroll progress timelines and view progress timelines. In our case, we are going to use the second one; a view progress timeline, and here is how MDN describes it:

You progress this timeline based on the change in visibility of an element (known as the subject) inside a scroller. The visibility of the subject inside the scroller is tracked as a percentage of progress — by default, the timeline is at 0% when the subject is first visible at one edge of the scroller, and 100% when it reaches the opposite edge.

You can check out the CSS-Tricks almanac definition for view-timeline-name while you’re at it for another explanation.

Things start to make more sense if we consider the thumb element as the subject and the input element as the scroller. After all, the thumb moves within the input area, so its visibility changes. We can track that movement as a percentage of progress and convert it to a value we can use to style the input element. We are essentially going to implement the equivalent of document.querySelector("input").value in JavaScript but with vanilla CSS!

The implementation

Now that we have an idea of how this works, let’s see how everything translates into code.

@property --val {
  syntax: "<number>";
  inherits: true;
  initial-value: 0; 
}

input[type="range"] {
  --min: attr(min type(<number>));
  --max: attr(max type(<number>));

  timeline-scope: --val;
  animation: --val linear both;
  animation-timeline: --val;
  animation-range: entry 100% exit 0%;
  overflow: hidden;
}

@keyframes --val {
  0%   { --val: var(--max) }
  100% { --val: var(--min) }
}

input[type="range"]::thumb {
  view-timeline: --val inline;
}

I know, this is a lot of strange syntax! But we will dissect each line and you will see that it’s not all that complex at the end of the day.

The subject and the scroller

We start by defining the subject, i.e. the thumb element, and for this we use the view-timeline shorthand property. From the MDN page, we can read:

The view-timeline CSS shorthand property is used to define a named view progress timeline, which is progressed through based on the change in visibility of an element (known as the subject) inside a scrollable element (scroller). view-timeline is set on the subject.

I think it’s self-explanatory. The view timeline name is --val and the axis is inline since we’re working along the horizontal x-axis.

Next, we define the scroller, i.e. the input element, and for this, we use overflow: hidden (or overflow: auto). This part is the easiest but also the one you will forget the most so let me insist on this: don’t forget to define overflow on the scroller!

I insist on this because your code will work fine without defining overflow, but the values won’t be good. The reason is that the scroller exists but will be defined by the browser (depending on your page structure and your CSS) and most of the time it’s not the one you want. So let me repeat it another time: remember the overflow property!

The animation

Next up, we create an animation that animates the --val variable between the input’s min and max values. Like we did in the first article, we are using the newly-enhanced attr() function to get those values. See that? The “animation” part of the scroll-driven animation, an animation we link to the view timeline we defined on the subject using animation-timeline. And to be able to animate a variable we register it using @property.

Note the use of timeline-scope which is another tricky feature that’s easy to overlook. By default, named view timelines are scoped to the element where they are defined and its descendant. In our case, the input is a parent element of the thumb so it cannot access the named view timeline. To overcome this, we increase the scope using timeline-scope. Again, from MDN:

timeline-scope is given the name of a timeline defined on a descendant element; this causes the scope of the timeline to be increased to the element that timeline-scope is set on and any of its descendants. In other words, that element and any of its descendant elements can now be controlled using that timeline.

Never forget about this! Sometimes everything is correctly defined but nothing is working because you forget about the scope.

There’s something else you might be wondering:

Why are the keyframes values inverted? Why is the min is set to 100% and the max set to 0%?

To understand this, let’s first take the following example where you can scroll the container horizontally to reveal a red circle inside of it.

Initially, the red circle is hidden on the right side. Once we start scrolling, it appears from the right side, then disappears to the left as you continue scrolling towards the right. We scroll from left to right but our actual movement is from right to left.

In our case, we don’t have any scrolling since our subject (the thumb) will not overflow the scroller (the input) but the main logic is the same. The starting point is the right side and the ending point is the left side. In other words, the animation starts when the thumb is on the right side (the input’s max value) and will end when it’s on the left side (the input’s min value).

The animation range

The last piece of the puzzle is the following important line of code:

animation-range: entry 100% exit 0%;

By default, the animation starts when the subject starts to enter the scroller from the right and ends when the subject has completely exited the scroller from the left. This is not good because, as we said, the thumb will not overflow the scroller, so it will never reach the start and the end of the animation.

Red circle representing the thumb element is at the far right of the container but not quite all the way there.

To rectify this we use the animation-range property to make the start of the animation when the subject has completely entered the scroller from the right (entry 100%) and the end of the animation when the subject starts to exit the scroller from the left (exit 0%).

Red circle representing the thumb element displayed qt the far right of the container where the animation starts.

To summarize, the thumb element will move within input’s area and that movement is used to control the progress of an animation that animates a variable between the input’s min and max attribute values. We have our replacement for document.querySelector("input").value in JavaScript!

What’s going on with all the --val instances everywhere? Is it the same thing each time?

I am deliberately using the same --val everywhere to confuse you a little and push you to try to understand what is going on. We usually use the dashed ident (--) notation to define custom properties (also called CSS variables) that we later call with var(). This is still true but that same notation can be used to name other things as well.

In our examples we have three different things named --val:

  1. The variable that is animated and registered using @property. It contains the selected value and is used to style the input.
  2. The named view timeline defined by view-timeline and used by animation-timeline.
  3. The keyframes named --val and called by animation.

Here is the same code written with different names for more clarity:

@property --val {
  syntax: "<number>";
  inherits: true;
  initial-value: 0; 
}

input[type="range"] {
  --min: attr(min type(<number>));
  --max: attr(max type(<number>));

  timeline-scope: --timeline;
  animation: value_update linear both;
  animation-timeline: --timeline;
  animation-range: entry 100% exit 0%;
  overflow: hidden;
}

@keyframes value_update {
  0%   { --val: var(--max) }
  100% { --val: var(--min) }
}

input[type="range"]::thumb {
  view-timeline: --timeine inline;
}

The star rating component

All that we have done up to now is get the selected value of the input range — which is honestly about 90% of the work we need to do. What remains is some basic styles and code taken from what we made in the first article.

If we omit the code from the previous section and the code from the previous article here is what we are left with:

input[type="range"] {
  background: 
    linear-gradient(90deg,
      hsl(calc(30 + 4 * var(--val)) 100% 56%) calc(var(--val) * 100% / var(--max)),
      #7b7b7b 0
    );
}
input[type="range"]::thumb {
  opacity: 0;
}

We make the thumb invisible and we define a gradient on the main element to color in the stars. No surprise here, but the gradient uses the same --val variable that contains the selected value to inform how much is colored in.

When, for example, you select three stars, the --val variable will equal 3 and the color stop of the first color will equal 3*100%/5 , or 60%, meaning three stars are colored in. That same color is also dynamic as I am using the hsl() function where the first argument (the hue) is a function of --val as well.

Here is the full demo, which you will want to open in Chrome 115+ at the time I’m writing this:

And guess what? This implementation works with half stars as well without the need to change the CSS. All you have to do is update the input’s attributes to work in half increments. Remember, we’re yanking these values out of HTML into CSS using attr(), which reads the attributes and returns them to us.

<input type="range" min=".5" step=".5" max="5">

That’s it! We have our rating star component that you can easily control by adjusting the attributes.

So, should I use border-image or a scroll-driven animation?

If we look past the browser support factor, I consider this version better than the border-image approach we used in the first article. The border-image version is simpler and does the job pretty well, but it’s limited in what it can do. While our goal is to create a star rating component, it’s good to be able to do more and be able to style an input range as you want.

With scroll-driven animations, we have more flexibility since the idea is to first get the value of the input and then use it to style the element. I know it’s not easy to grasp but don’t worry about that. You will face scroll-driven animations more often in the future and it will become more familiar with time. This example will look easy to you in good time.

Worth noting, that the code used to get the value is a generic code that you can easily reuse even if you are not going to style the input itself. Getting the value of the input is independent of styling it.

Here is a demo where I am adding a tooltip to a range slider to show its value:

Many techniques are involved to create that demo and one of them is using scroll-driven animations to get the input value and show it inside the tooltip!

Here is another demo using the same technique where different range sliders are controlling different variables on the page.

And why not a wavy range slider?

This one is a bit crazy but it illustrates how far we go with styling an input range! So, even if your goal is not to create a star rating component, there are a lot of use cases where such a technique can be really useful.

Conclusion

I hope you enjoyed this brief two-part series. In addition to a star rating component made with minimal code, we have explored a lot of cool and modern features, including the attr() function, CSS mask, and scroll-driven animations. It’s still early to adopt all of these features in production because of browser support, but it’s a good time to explore them and see what can be done soon using only CSS.

Article series

  1. A CSS-Only Star Rating Component and More! (Part 1)
  2. A CSS-Only Star Rating Component and More! (Part 2)


A CSS-Only Star Rating Component and More! (Part 2) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

0 Comments

Recommended Comments

There are no comments to display.

Important Information

Terms of Use Privacy Policy Guidelines We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.