Blog Ratings UI Alt2

Progressively enhance your star ratings with modern CSS

Written by Paul Woods.
Published on 28 September 2026.

Let’s build a pro­gres­sive­ly enhanced star rat­ings UI. Enhance­ments are going to come from three new CSS func­tions: color-mix(), sibling-index() and sibling-count().

Contents
Rate this UI rating scale showing five rows of 1 to 5 star ratings with color-coded active states on a dark background.

I’ll intro­duce these new func­tions first, and then we’ll dive into build­ing our component.

color-mix()

The color-mix() func­tion is defined in the CSS Col­or Mod­ule Lev­el 5 spec­i­fi­ca­tion. It takes a colour space (eg. srgb, hsl, oklch, etc.), two colours and per­cent­ages for mix­ing. These can be any ​“type” of colour: hex val­ues, colour names, key­words like transparent.

It looks a bit like this:

.no-mix { background-color: red; }
.mix-1 { background-color: color-mix(in hsl, red 50%, transparent 50%); }
.mix-2 { background-color: color-mix(in hsl, red 50%, #0f0 50%); }

/* Percentage can be added to either one or both of the values
A bar of three colours which are made up of mixing red, transparency and green.

Pure red. Red mixed with 50% transparency. Red mixed with 50% green.

sibling-index() and sibling-count()

These are pret­ty new in the CSS World. But I’m writ­ing this blog with con­fi­dence now because Fire­fox sup­ports sibling-index() and sibling-count(). So, what are they? Each returns a number:

sibling-count(): Rep­re­sents the num­ber of sib­lings for an ele­ment, includ­ing the ele­ment itself.

<style>
  li { --count: sibling-count(); }
</style>
<ul>
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
  <li>Item 4</li>
</ul>

In the above exam­ple, --count would be cal­cu­lat­ed as 4.

sibling-index() for an ele­ment returns where in the sequence it lives.

<style>
  .special { --index: sibling-index(); }
</style>
<ul>
  <li>Item 1</li>
  <li>Item 2</li>
  <li class="special">Item 3</li>
  <li>Item 4</li>
</ul>

In the above exam­ple, --index would be cal­cu­lat­ed as 3.

Build­ing the rat­ings UI 

Keep the above tech­niques in the back of your mind. Next, we’ll build the UI, using a series of radio inputs.

<label>
  <input type="radio" name="rating" value="1" />
  1 star
</label>
<label>
  <input type="radio" name="rating" value="2" />
   2 stars
</label>
<label>
  <input type="radio" name="rating" value="3" />
  3 stars
</label>
<label>
  <input type="radio" name="rating" value="4" />
  4 stars
</label>
<label>
  <input type="radio" name="rating" value="5" />
  5 stars
</label>
Example UI with five simple radio buttons labelled from 1 star to 5 stars.

The bare radio but­tons and the text look a lit­tle uninvit­ing, so we’ll hide the text and radio inputs visu­al­ly and add star icons. We’ll style the labels to give them a but­ton-like appear­ance. Don’t for­get hov­er, active and focus states for these! Let’s also wrap them in a <div> so we can apply a flex dis­play with gaps.

See the Pen Ratings UI (Part 1) by Paul Woods (@paleweasel) on CodePen.

We’re get­ting some­where. This is start­ing to look pret­ty good! One issue here is that when you tap on a star, you’re real­ly only light­ing up 1. We can fix this with a few:has() pseu­do-class­es com­bined with sib­ling (~) selec­tors, no problem.

Bring­ing in the big-guns 

Let’s take what we’ve learnt about color-mix() and the sibling-*() func­tions and bring in a lit­tle colour. To do this, we’re going to set two colours. They can be any­thing, but we’ll go with red for one star and green for 5 stars. We can set these two val­ues in:root.

:root {
  --color-bad: red;
  --color-good: limegreen;
}

For each but­ton, the colour can be a mix­ture of red and green. I’ve cho­sen the OKLCH colour space. This ensures a con­sis­tent con­trast as the hue changes.

.button {
  background-color: color-mix(
    in oklch,
    var(--color-bad),
    var(--color-good) 50%
  );
}

This will give a yel­low­ish colour because it’s mix­ing red with 50% green. This works great for the mid­dle but­ton, but we want to ensure the per­cent­age can change depend­ing on where the but­ton sits in the rat­ings panel.

Let’s bring in our sibling-*() functions.

.button {
  background-color: color-mix(
    in oklch,
    var(--color-bad),
    var(--color-good)
    calc(
      (sibling-index() - 1)
      / (sibling-count() - 1)
      * 100%
    )
  )
}

What’s hap­pen­ing here? So just divid­ing the index by the count and mul­ti­ply­ing by 100% doesn’t get you exact­ly what you need. The first but­ton ends up hav­ing 20% green. And the last but­ton has only 80%, rather than the first and last but­tons being full colour.

Minus 1 on the first item means the first star gets 0, which returns 0%. Minus 1 on the index means the last item gets 4/4 * 100%, which, of course, is 100%.

You can play with the result of this in this CodePen:

See the Pen Ratings UI (Complete) by Paul Woods (@paleweasel) on CodePen.

Pro­gres­sive­ly enhanced! 

Before we fin­ish, in the exam­ple, the inputs are wrapped in a fieldset with a legend. This ensures that assis­tive tech­nol­o­gy can ful­ly describe the radio but­tons to the user.

The advan­tages of the Mod­ern CSS approach include:

  1. Hav­ing as many stars as you like. The colour mix­es will adapt.
  2. Choos­ing your own colours. You can also have dark-mode ver­sions with either a media query or the light-dark() function.
  3. A fall­back to a still-usable solu­tion with­out the rain­bow colours: a nice pro­gres­sive enhancement.

What’s the takeaway? 

As you can see, build­ing things requires a lot less JavaScript. Of course, this still has a pur­pose, but quite often it serves as a polyfill.

Now, these new CSS prop­er­ties pro­vide real brows­er-pow­ered solu­tions that ensure few­er mag­ic num­bers and much more robust tech­niques. They also let you use struc­tured HTML where pos­si­ble and keep the code­base slim.

My wish­list

Whilst, to me, this lit­tle UI serves as a cool use-case for new tools like sibling-count(), it makes me wish for more. I would love, for instance, that I could make the set of stars take the colour of the last one cho­sen. Hav­ing them all turn green when the last one’s select­ed would be the icing on the cake. But we’re not quite there yet.

One of the ways we will get there is by using these tech­niques in our work and, maybe, writ­ing about it. The web com­mu­ni­ty is sur­pris­ing­ly close-knit.

Back to top