Tutorials 4 min read

How to create a rating system with Tailwind CSS: JavaScript and Alpine.js

Build a five-star rating system with Tailwind CSS, two ways: vanilla JavaScript or Alpine.js. Click to rate, click again to clear, with a live label.

It’s Friday! And today we are creating a five-star rating system. We’ll build it twice, first with vanilla JavaScript, then with Alpine.js, so you can pick whichever fits your stack.

What is a rating system?

A rating system lets users score content, usually with a row of stars, and helps other people make informed decisions about what they are viewing. Ours has five stars: clicking one sets the rating, clicking the same one again clears it, and a label spells out the current score.

Use cases

  • E-commerce: let shoppers rate products so others can buy with confidence.
  • Movies and books: collect scores on how much people enjoyed a title.
  • Services: gather feedback on support, deliveries, or bookings.
  • Content sites: surface the most appreciated articles or recipes.

The markup

  • id="ratingApp": the container that holds the rating system.
  • id="starsContainer": the container that holds the stars.
  • id="currentRating": the element that displays the current rating.
html
<div id="ratingApp">
  <div id="starsContainer">
    <svg
      class="... text-zinc-500"
      xmlns="http://www.w3.org/2000/svg"
      stroke="currentColor"
    ></svg>
  </div>
  <div class="mt-2">
    <span id="currentRating">0 stars</span>
  </div>
</div>

The script

  • document.addEventListener("DOMContentLoaded", function () {: runs the code once the page is loaded.
  • const stars = document.querySelectorAll("#starsContainer svg"); and const currentRating = document.getElementById("currentRating");: select the stars and the rating label.
  • let rating = -1;: stores the current rating, -1 means nothing is selected.
  • const updateRatingDisplay = () => {: writes “0 stars” or the star count into the label, then toggles text-orange-500 on the stars up to the rating and text-zinc-500 on the rest.
  • star.addEventListener("click", () => {: sets the rating to the clicked star, or clears it back to -1 when you click the same star twice, then refreshes the display.
js
document.addEventListener("DOMContentLoaded", function () {
  const stars = document.querySelectorAll("#starsContainer svg");
  const currentRating = document.getElementById("currentRating");

  let rating = -1;

  const updateRatingDisplay = () => {
    currentRating.textContent =
      rating === -1 ? "0 stars" : rating + 1 + " stars";
    stars.forEach((star, index) => {
      star.classList.toggle("text-orange-500", index <= rating);
      star.classList.toggle("text-zinc-500", index > rating);
    });
  };

  stars.forEach((star, index) => {
    star.addEventListener("click", () => {
      rating = rating === index ? -1 : index;
      updateRatingDisplay();
    });
  });

  updateRatingDisplay();
});

The Alpine.js version

Same component, no separate script: the state lives in x-data.

  • x-data="{ rating: -1 }": stores the rating.
  • <template x-for="(star, index) in [0, 1, 2, 3, 4]" :key="index">: loops out the five stars.
  • @click="rating = (rating === index) ? -1 : index": sets the rating when a star is clicked, or clears it when the same star is clicked again.
  • :class="{ 'text-orange-500': index <= rating, 'text-zinc-500': index > rating }": colors the stars up to the current rating.
  • <span x-text="(rating === -1) ? '0 stars' : rating + 1 + ' stars'">: displays the current rating.

Classes are removed for brevity, but I’ll keep those classes relevant to the tutorial.

html
<div x-data="{ rating: -1 }">
  <!-- Display Stars -->
  <div>
    <template x-for="(star, index) in [0, 1, 2, 3, 4]" :key="index">
      <svg
        @click="rating = (rating === index) ? -1 : index"
        :class="{ 'text-orange-500': index <= rating, 'text-zinc-500': index > rating }"
      >
        <!-- SVG Path goes here-->
      </svg>
    </template>
  </div>

  <!-- Display Current Rating -->
  <div class="mt-2">
    <span
      x-text="(rating === -1) ? '0 stars' : rating + 1 + ' stars'"
      class="text-zinc-500"
    ></span>
  </div>
</div>

You can try the Alpine version separately: live demo and source code.

Which one should you use?

Alpine’s x-for renders the stars and keeps the coloring declarative, which is nice when it’s already in your stack. The vanilla version does the same with a small loop and works without any dependency.

Conclusion

Five stars, one piece of state, and a couple of class toggles: that’s the whole rating system. It works for movies, books, products, or anything else your users might want to score.

Hope you enjoyed this tutorial and have a great day!

/Michael Andreuzza