Scalable "star rating" without JS (and no SVG or image for the star)
Tags: css, html, webdev, beginners
Published: 2021-07-04 15:02:32

Here I am with my "Star rating" system:

✔️ No JavaScript.
✔️ No complex HTML code, only the needed `<input>` elements for the user interaction and one extra element.
✔️ No hacky CSS code. 
✔️ Easily scalable. Simply add more `<input>` to get more stars. No need to change any CSS code.
✔️ Works with keyboard navigation.
✔️ No SVG, No images. The star shape is built using pure CSS.
✔️ You can easily adjust the size and coloration of the stars.
✔️ Support both `ltr` and `rtl` direction

See it in play:

{% codepen https://codepen.io/t_afif/pen/VwbvyLm %}

As said, the HTML code is pretty simple. A `div` that contains our inputs and an extra `<i>` element. Nothing more!

All you have to do is to add as many inputs as stars you want.

For the CSS part, we have the trivial one as follow:

```css
.stars {
  --s:50px;
  position:relative;
  display:inline-flex;
}
.stars input {
  width:var(--s);
  height:var(--s);
  margin:0;
  opacity:0;
  cursor:pointer;
}
```

The variable `--s` will define the size of our inputs that we make invisible using `opacity:0`. Everything is within a flexbox container (an `inline` one to easily integrate the star rating like an image or a simple text).

The real trick relies on `i`

```css
.stars i {
  position:absolute;
  inset:0 0 calc(var(--s)*0.1);
  pointer-events:none;
  /* the star */
  --v1:transparent,#000 0.5deg 108deg,#0000 109deg;
  --v2:transparent,#000 0.5deg  36deg,#0000  37deg;
  -webkit-mask:
    conic-gradient(from 54deg  at calc(var(--s)*0.68) calc(var(--s)*0.57),var(--v1)),
    conic-gradient(from 90deg  at calc(var(--s)*0.02) calc(var(--s)*0.35),var(--v2)),
    conic-gradient(from 126deg at calc(var(--s)*0.5)  calc(var(--s)*0.7) ,var(--v1)),
    conic-gradient(from 162deg at calc(var(--s)*0.5)  0                  ,var(--v2));
  -webkit-mask-size: var(--s) var(--s);
  -webkit-mask-composite: xor,destination-over;
  mask-composite: exclude,add;
  /**/
  background:
    linear-gradient(rgba(255,0,0,var(--o,0.3)) 0 0),  
    linear-gradient(gold                       0 0)
    #ccc;
  background-size:
     calc(var(--l,0)*var(--s)) 100%,
     calc(var(--p,0)*var(--s)) 100%;
  background-repeat:no-repeat;
}
```

First, we make it an absolute element that will cover all the div and logically all the inputs. We use `pointer-events:none;` to be able to interact with the inputs but still have the mouse cursor on the `<i>`.

Second, we apply 3 background layers as follows:
1. The bottom layer is a grey coloration (`#ccc`) to indicate the number of stars and the non-selected ones
2. The middle layer is the gold coloration. Here we use a gradient having a variable size based on the selected stars (controlled with the variable `--p`)
3. The top layer is similar to (2) and will respond to the `:hover` effect (controlled with the variable `--l`). I will be using a semi-transparent color so we can still see the selected stars.

#### What about all those strange gradients and mask??

This is my personal touch and the crazy part of the work. I have built the star shape using multiple gradients applied to the `mask` property so all the background layers are seen through that shape.

Finally, the interactive part done using the following code:

```css
.stars:focus-within {
  outline:1px solid;
}
input:active ~ i{--o:1}
input:nth-of-type(N):checked ~ i {--p:N} 
input:nth-of-type(N):hover ~ i {--l:N}
```

`:focus-within` will allow me to style the whole `div` when interacting with the inputs (good for accessibility)

When an input is active (clicked on) I change the semi-transparent color to an opaque one to highlight the click action.

On `:checked` I update the variable `--p` based on the input index. We can easily generate the code using SASS/LESS or by doing some copy/paste (it only takes a few seconds to write the code that can cover up to 20 inputs)

On `:hover` we do the same logic but with the variable `--l`.

### What about the `rtl` support?

Either we update the `background-position` based on the direction attribute and we simply add:

```css
[dir="rtl"] .stars i {
    background-position: right;
}
```
Or I update the code and instead of multiple backgrounds, I  rely on pseudo-elements that I can easily place using `margin-inline-end`

{% codepen https://codepen.io/t_afif/pen/eYWJNBd %}

The trick is to have both pseudo-elements above each other (thanks to `grid-area:1/1`) with the adequate color. Their width will be controlled with the same variables used to control the gradient. Finally by using `margin-inline-end:auto;` they will get placed either at the `left` or the `right` based on the direction.

---

### That's it

A simple non-hacky code and we have a fully interactive "Star rating" that you can easily embed anywhere.

---

### Bonus

If you don't need the interactive part, here is a one-div version that you can control using CSS variables:

{% codepen https://codepen.io/t_afif/pen/wvdKRPx %}