This tutorial will guide you through the process of creating a Star Rating Animation Using Svg And Vanilla Js. This interactive element enhances user experience by providing visual feedback when a rating is selected, making it more engaging than a static rating system. By using SVG and vanilla JavaScript, you’ll achieve a lightweight and customizable solution without relying on external libraries.
Setting Up the HTML Structure
First, you’ll need to structure the HTML to create the star rating component. This involves creating radio inputs for each star, along with corresponding SVG elements for the star graphics and labels for accessibility.
<form class="rating">
<div class="rating__stars">
<input id="rating-1" class="rating__input rating__input-1" type="radio" name="rating" value="1">
<input id="rating-2" class="rating__input rating__input-2" type="radio" name="rating" value="2">
<input id="rating-3" class="rating__input rating__input-3" type="radio" name="rating" value="3">
<input id="rating-4" class="rating__input rating__input-4" type="radio" name="rating" value="4">
<input id="rating-5" class="rating__input rating__input-5" type="radio" name="rating" value="5">
<label class="rating__label" for="rating-1">
<svg class="rating__star" width="32" height="32" viewBox="0 0 32 32" aria-hidden="true">
<g transform="translate(16,16)">
<circle class="rating__star-ring" fill="none" stroke="#000" stroke-width="16" r="8" transform="scale(0)" />
</g>
<g stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<g transform="translate(16,16) rotate(180)">
<polygon class="rating__star-stroke" points="0,15 4.41,6.07 14.27,4.64 7.13,-2.32 8.82,-12.14 0,-7.5 -8.82,-12.14 -7.13,-2.32 -14.27,4.64 -4.41,6.07" fill="none" />
<polygon class="rating__star-fill" points="0,15 4.41,6.07 14.27,4.64 7.13,-2.32 8.82,-12.14 0,-7.5 -8.82,-12.14 -7.13,-2.32 -14.27,4.64 -4.41,6.07" fill="#000" />
</g>
<g transform="translate(16,16)" stroke-dasharray="12 12" stroke-dashoffset="12">
<polyline class="rating__star-line" transform="rotate(0)" points="0 4,0 16" />
<polyline class="rating__star-line" transform="rotate(72)" points="0 4,0 16" />
<polyline class="rating__star-line" transform="rotate(144)" points="0 4,0 16" />
<polyline class="rating__star-line" transform="rotate(216)" points="0 4,0 16" />
<polyline class="rating__star-line" transform="rotate(288)" points="0 4,0 16" />
</g>
</g>
</svg>
<span class="rating__sr">1 star—Terrible</span>
</label>
<label class="rating__label" for="rating-2">
<svg class="rating__star" width="32" height="32" viewBox="0 0 32 32" aria-hidden="true">
<g transform="translate(16,16)">
<circle class="rating__star-ring" fill="none" stroke="#000" stroke-width="16" r="8" transform="scale(0)" />
</g>
<g stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<g transform="translate(16,16) rotate(180)">
<polygon class="rating__star-stroke" points="0,15 4.41,6.07 14.27,4.64 7.13,-2.32 8.82,-12.14 0,-7.5 -8.82,-12.14 -7.13,-2.32 -14.27,4.64 -4.41,6.07" fill="none" />
<polygon class="rating__star-fill" points="0,15 4.41,6.07 14.27,4.64 7.13,-2.32 8.82,-12.14 0,-7.5 -8.82,-12.14 -7.13,-2.32 -14.27,4.64 -4.41,6.07" fill="#000" />
</g>
<g transform="translate(16,16)" stroke-dasharray="12 12" stroke-dashoffset="12">
<polyline class="rating__star-line" transform="rotate(0)" points="0 4,0 16" />
<polyline class="rating__star-line" transform="rotate(72)" points="0 4,0 16" />
<polyline class="rating__star-line" transform="rotate(144)" points="0 4,0 16" />
<polyline class="rating__star-line" transform="rotate(216)" points="0 4,0 16" />
<polyline class="rating__star-line" transform="rotate(288)" points="0 4,0 16" />
</g>
</g>
</svg>
<span class="rating__sr">2 stars—Bad</span>
</label>
<label class="rating__label" for="rating-3">
<svg class="rating__star" width="32" height="32" viewBox="0 0 32 32" aria-hidden="true">
<g transform="translate(16,16)">
<circle class="rating__star-ring" fill="none" stroke="#000" stroke-width="16" r="8" transform="scale(0)" />
</g>
<g stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<g transform="translate(16,16) rotate(180)">
<polygon class="rating__star-stroke" points="0,15 4.41,6.07 14.27,4.64 7.13,-2.32 8.82,-12.14 0,-7.5 -8.82,-12.14 -7.13,-2.32 -14.27,4.64 -4.41,6.07" fill="none" />
<polygon class="rating__star-fill" points="0,15 4.41,6.07 14.27,4.64 7.13,-2.32 8.82,-12.14 0,-7.5 -8.82,-12.14 -7.13,-2.32 -14.27,4.64 -4.41,6.07" fill="#000" />
</g>
<g transform="translate(16,16)" stroke-dasharray="12 12" stroke-dashoffset="12">
<polyline class="rating__star-line" transform="rotate(0)" points="0 4,0 16" />
<polyline class="rating__star-line" transform="rotate(72)" points="0 4,0 16" />
<polyline class="rating__star-line" transform="rotate(144)" points="0 4,0 16" />
<polyline class="rating__star-line" transform="rotate(216)" points="0 4,0 16" />
<polyline class="rating__star-line" transform="rotate(288)" points="0 4,0 16" />
</g>
</g>
</svg>
<span class="rating__sr">3 stars—OK</span>
</label>
<label class="rating__label" for="rating-4">
<svg class="rating__star" width="32" height="32" viewBox="0 0 32 32" aria-hidden="true">
<g transform="translate(16,16)">
<circle class="rating__star-ring" fill="none" stroke="#000" stroke-width="16" r="8" transform="scale(0)" />
</g>
<g stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<g transform="translate(16,16) rotate(180)">
<polygon class="rating__star-stroke" points="0,15 4.41,6.07 14.27,4.64 7.13,-2.32 8.82,-12.14 0,-7.5 -8.82,-12.14 -7.13,-2.32 -14.27,4.64 -4.41,6.07" fill="none" />
<polygon class="rating__star-fill" points="0,15 4.41,6.07 14.27,4.64 7.13,-2.32 8.82,-12.14 0,-7.5 -8.82,-12.14 -7.13,-2.32 -14.27,4.64 -4.41,6.07" fill="#000" />
</g>
<g transform="translate(16,16)" stroke-dasharray="12 12" stroke-dashoffset="12">
<polyline class="rating__star-line" transform="rotate(0)" points="0 4,0 16" />
<polyline class="rating__star-line" transform="rotate(72)" points="0 4,0 16" />
<polyline class="rating__star-line" transform="rotate(144)" points="0 4,0 16" />
<polyline class="rating__star-line" transform="rotate(216)" points="0 4,0 16" />
<polyline class="rating__star-line" transform="rotate(288)" points="0 4,0 16" />
</g>
</g>
</svg>
<span class="rating__sr">4 stars—Good</span>
</label>
<label class="rating__label" for="rating-5">
<svg class="rating__star" width="32" height="32" viewBox="0 0 32 32" aria-hidden="true">
<g transform="translate(16,16)">
<circle class="rating__star-ring" fill="none" stroke="#000" stroke-width="16" r="8" transform="scale(0)" />
</g>
<g stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<g transform="translate(16,16) rotate(180)">
<polygon class="rating__star-stroke" points="0,15 4.41,6.07 14.27,4.64 7.13,-2.32 8.82,-12.14 0,-7.5 -8.82,-12.14 -7.13,-2.32 -14.27,4.64 -4.41,6.07" fill="none" />
<polygon class="rating__star-fill" points="0,15 4.41,6.07 14.27,4.64 7.13,-2.32 8.82,-12.14 0,-7.5 -8.82,-12.14 -7.13,-2.32 -14.27,4.64 -4.41,6.07" fill="#000" />
</g>
<g transform="translate(16,16)" stroke-dasharray="12 12" stroke-dashoffset="12">
<polyline class="rating__star-line" transform="rotate(0)" points="0 4,0 16" />
<polyline class="rating__star-line" transform="rotate(72)" points="0 4,0 16" />
<polyline class="rating__star-line" transform="rotate(144)" points="0 4,0 16" />
<polyline class="rating__star-line" transform="rotate(216)" points="0 4,0 16" />
<polyline class="rating__star-line" transform="rotate(288)" points="0 4,0 16" />
</g>
</g>
</svg>
<span class="rating__sr">5 stars—Excellent</span>
</label>
<p class="rating__display" data-rating="1" hidden>Terrible</p>
<p class="rating__display" data-rating="2" hidden>Bad</p>
<p class="rating__display" data-rating="3" hidden>OK</p>
<p class="rating__display" data-rating="4" hidden>Good</p>
<p class="rating__display" data-rating="5" hidden>Excellent</p>
</div>
</form>
<script src="./script.js"></script>
Styling with CSS
Next, apply CSS styles to visually present the star rating and implement the animations. This includes styling the stars, radio inputs, and labels, as well as defining the keyframe animations for the visual effects.
* {
border: 0;
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #e3e4e8;
--fg: #17181c;
--primary: #255ff4;
--yellow: #f4a825;
--yellow-t: rgba(244, 168, 37, 0);
--bezier: cubic-bezier(0.42,0,0.58,1);
--trans-dur: 0.3s;
font-size: calc(24px + (30 - 24) * (100vw - 320px) / (1280 - 320));
}
body {
background-color: var(--bg);
color: var(--fg);
font: 1em/1.5 "DM Sans", sans-serif;
display: flex;
height: 100vh;
transition: background-color var(--trans-dur), color var(--trans-dur);
}
.rating {
margin: auto;
}
.rating__display {
font-size: 1em;
font-weight: 500;
min-height: 1.25em;
position: absolute;
top: 100%;
width: 100%;
text-align: center;
}
.rating__stars {
display: flex;
padding-bottom: 0.375em;
position: relative;
}
.rating__star {
display: block;
overflow: visible;
pointer-events: none;
width: 2em;
height: 2em;
}
.rating__star-ring, .rating__star-fill, .rating__star-line, .rating__star-stroke {
animation-duration: 1s;
animation-timing-function: ease-in-out;
animation-fill-mode: forwards;
}
.rating__star-ring, .rating__star-fill, .rating__star-line {
stroke: var(--yellow);
}
.rating__star-fill {
fill: var(--yellow);
transform: scale(0);
transition: fill var(--trans-dur) var(--bezier), transform var(--trans-dur) var(--bezier);
}
.rating__star-line {
stroke-dasharray: 12 13;
stroke-dashoffset: -13;
}
.rating__star-stroke {
stroke: #c7cad1;
transition: stroke var(--trans-dur);
}
.rating__label {
cursor: pointer;
padding: 0.125em;
}
.rating__label--delay1 .rating__star-ring, .rating__label--delay1 .rating__star-fill, .rating__label--delay1 .rating__star-line, .rating__label--delay1 .rating__star-stroke {
animation-delay: 0.05s;
}
.rating__label--delay2 .rating__star-ring, .rating__label--delay2 .rating__star-fill, .rating__label--delay2 .rating__star-line, .rating__label--delay2 .rating__star-stroke {
animation-delay: 0.1s;
}
.rating__label--delay3 .rating__star-ring, .rating__label--delay3 .rating__star-fill, .rating__label--delay3 .rating__star-line, .rating__label--delay3 .rating__star-stroke {
animation-delay: 0.15s;
}
.rating__label--delay4 .rating__star-ring, .rating__label--delay4 .rating__star-fill, .rating__label--delay4 .rating__star-line, .rating__label--delay4 .rating__star-stroke {
animation-delay: 0.2s;
}
.rating__input {
position: absolute;
-webkit-appearance: none;
appearance: none;
}
.rating__input:hover ~ [data-rating]:not([hidden]) {
display: none;
}
.rating__input-1:hover ~ [data-rating="1"][hidden], .rating__input-2:hover ~ [data-rating="2"][hidden], .rating__input-3:hover ~ [data-rating="3"][hidden], .rating__input-4:hover ~ [data-rating="4"][hidden], .rating__input-5:hover ~ [data-rating="5"][hidden], .rating__input:checked:hover ~ [data-rating]:not([hidden]) {
display: block;
}
.rating__input-1:hover ~ .rating__label:first-of-type .rating__star-stroke, .rating__input-2:hover ~ .rating__label:nth-of-type(-n + 2) .rating__star-stroke, .rating__input-3:hover ~ .rating__label:nth-of-type(-n + 3) .rating__star-stroke, .rating__input-4:hover ~ .rating__label:nth-of-type(-n + 4) .rating__star-stroke, .rating__input-5:hover ~ .rating__label:nth-of-type(-n + 5) .rating__star-stroke {
stroke: var(--yellow);
transform: scale(1);
}
.rating__input-1:checked ~ .rating__label:first-of-type .rating__star-ring, .rating__input-2:checked ~ .rating__label:nth-of-type(-n + 2) .rating__star-ring, .rating__input-3:checked ~ .rating__label:nth-of-type(-n + 3) .rating__star-ring, .rating__input-4:checked ~ .rating__label:nth-of-type(-n + 4) .rating__star-ring, .rating__input-5:checked ~ .rating__label:nth-of-type(-n + 5) .rating__star-ring {
animation-name: starRing;
}
.rating__input-1:checked ~ .rating__label:first-of-type .rating__star-stroke, .rating__input-2:checked ~ .rating__label:nth-of-type(-n + 2) .rating__star-stroke, .rating__input-3:checked ~ .rating__label:nth-of-type(-n + 3) .rating__star-stroke, .rating__input-4:checked ~ .rating__label:nth-of-type(-n + 4) .rating__star-stroke, .rating__input-5:checked ~ .rating__label:nth-of-type(-n + 5) .rating__star-stroke {
animation-name: starStroke;
}
.rating__input-1:checked ~ .rating__label:first-of-type .rating__star-line, .rating__input-2:checked ~ .rating__label:nth-of-type(-n + 2) .rating__star-line, .rating__input-3:checked ~ .rating__label:nth-of-type(-n + 3) .rating__star-line, .rating__input-4:checked ~ .rating__label:nth-of-type(-n + 4) .rating__star-line, .rating__input-5:checked ~ .rating__label:nth-of-type(-n + 5) .rating__star-line {
animation-name: starLine;
}
.rating__input-1:checked ~ .rating__label:first-of-type .rating__star-fill, .rating__input-2:checked ~ .rating__label:nth-of-type(-n + 2) .rating__star-fill, .rating__input-3:checked ~ .rating__label:nth-of-type(-n + 3) .rating__star-fill, .rating__input-4:checked ~ .rating__label:nth-of-type(-n + 4) .rating__star-fill, .rating__input-5:checked ~ .rating__label:nth-of-type(-n + 5) .rating__star-fill {
animation-name: starFill;
}
.rating__input-1:not(:checked):hover ~ .rating__label:first-of-type .rating__star-fill, .rating__input-2:not(:checked):hover ~ .rating__label:nth-of-type(2) .rating__star-fill, .rating__input-3:not(:checked):hover ~ .rating__label:nth-of-type(3) .rating__star-fill, .rating__input-4:not(:checked):hover ~ .rating__label:nth-of-type(4) .rating__star-fill, .rating__input-5:not(:checked):hover ~ .rating__label:nth-of-type(5) .rating__star-fill {
fill: var(--yellow-t);
}
.rating__sr {
clip: rect(1px, 1px, 1px, 1px);
overflow: hidden;
position: absolute;
width: 1px;
height: 1px;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #17181c;
--fg: #e3e4e8;
}
.rating__star-stroke {
stroke: #454954;
}
}
@keyframes starRing {
from, 20% {
animation-timing-function: ease-in;
opacity: 1;
r: 8px;
stroke-width: 16px;
transform: scale(0);
}
35% {
animation-timing-function: ease-out;
opacity: 0.5;
r: 8px;
stroke-width: 16px;
transform: scale(1);
}
50%, to {
opacity: 0;
r: 16px;
stroke-width: 0;
transform: scale(1);
}
}
@keyframes starFill {
from, 40% {
animation-timing-function: ease-out;
transform: scale(0);
}
60% {
animation-timing-function: ease-in-out;
transform: scale(1.2);
}
80% {
transform: scale(0.9);
}
to {
transform: scale(1);
}
}
@keyframes starStroke {
from {
transform: scale(1);
}
20%, to {
transform: scale(0);
}
}
@keyframes starLine {
from, 40% {
animation-timing-function: ease-out;
stroke-dasharray: 1 23;
stroke-dashoffset: 1;
}
60%, to {
stroke-dasharray: 12 13;
stroke-dashoffset: -13;
}
}
Implementing the JavaScript Logic
Now, add the JavaScript to handle user interaction and trigger the animations. This involves creating a class to manage the star rating logic, handling the change event on the radio inputs, and updating the visual state of the stars based on the selected rating.
window.addEventListener("DOMContentLoaded",() => {
const starRating = new StarRating("form");
});
class StarRating {
constructor(qs) {
this.ratings = [
{id: 1, name: "Terrible"},
{id: 2, name: "Bad"},
{id: 3, name: "OK"},
{id: 4, name: "Good"},
{id: 5, name: "Excellent"}
];
this.rating = null;
this.el = document.querySelector(qs);
this.init();
}
init() {
this.el?.addEventListener("change",this.updateRating.bind(this));
// stop Firefox from preserving form data between refreshes
try {
this.el?.reset();
} catch (err) {
console.error("Element isn’t a form.");
}
}
updateRating(e) {
// clear animation delays
Array.from(this.el.querySelectorAll(`[for*="rating"]`)).forEach(el => {
el.className = "rating__label";
});
const ratingObject = this.ratings.find(r => r.id === +e.target.value);
const prevRatingID = this.rating?.id || 0;
let delay = 0;
this.rating = ratingObject;
this.ratings.forEach(rating => {
const { id } = rating;
// add the delays
const ratingLabel = this.el.querySelector(`[for="rating-${id}"]`);
if (id > prevRatingID + 1 && id <= this.rating.id) {
++delay;
ratingLabel.classList.add(`rating__label--delay${delay}`);
}
// hide ratings to not read, show the one to read
const ratingTextEl = this.el.querySelector(`[data-rating="${id}"]`);
if (this.rating.id !== id)
ratingTextEl.setAttribute("hidden",true);
else
ratingTextEl.removeAttribute("hidden");
});
}
}
Including Header Assets
To ensure proper rendering and styling, you’ll need to include the necessary header assets, such as meta tags for viewport settings and links to external stylesheets for fonts.
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><link rel='stylesheet' href='https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500&display=swap'>
Including Footer Assets
Finally, you need to include the javascript code.
In conclusion, by following this tutorial, you have successfully implemented a Star Rating Animation Using Svg And Vanilla Js, creating a visually appealing and interactive rating system for your website.







