TikTok Tutorial #9 - How to create an Animated Star Rating in CSS & JavaScript

Learn with us how to create an animated star rating using CSS and Javascript!

If you found us on TikTok on the following post, check out this article and copy-paste the full code!

Happy coding! šŸ˜»

@creative.tim You will find the link to the full code for this animated star rating in our bio šŸ¤© #webdev #programmingexercises #csscoding #techtok ā™¬ original sound - Creative Tim

HTML Code

Use this code to start your animated star rating!

<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>

CSS Code

Continue with the CSS code and add life to your star rating component!

* {
	border: 0;
	box-sizing: border-box;
	margin: 0;
	padding: 0;
}

$hue: 223;
$starHue: 38;

:root {
	--bg: #{hsl($hue,10%,90%)};
	--fg: #{hsl($hue,10%,10%)};
	--primary: #{hsl($hue,90%,55%)};
	--yellow: #{hsl($starHue,90%,55%)};
	--yellow-t: #{hsla($starHue,90%,55%,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;

	&__display {
		font-size: 1em;
		font-weight: 500;
		min-height: 1.25em;
		position: absolute;
		top: 100%;
		width: 100%;
		text-align: center;
	}
	&__stars {
		display: flex;
		padding-bottom: 0.375em;
		position: relative;
	}
	&__star {
		display: block;
		overflow: visible;
		pointer-events: none;
		width: 2em;
		height: 2em;

		&-ring,
		&-fill,
		&-line,
		&-stroke {
			animation-duration: 1s;
			animation-timing-function: ease-in-out;
			animation-fill-mode: forwards;
		}
		&-ring,
		&-fill,
		&-line {
			stroke: var(--yellow);
		}
		&-fill {
			fill: var(--yellow);
			transform: scale(0);
			transition:
				fill var(--trans-dur) var(--bezier),
				transform var(--trans-dur) var(--bezier);
		}
		&-stroke {
			stroke: hsl($hue,10%,80%);
			transition: stroke var(--trans-dur);
		}
	}
	&__label {
		cursor: pointer;
		padding: 0.125em;
	}
	@for $s from 1 through 4 {
		&__label--delay#{$s} &__star-ring,
		&__label--delay#{$s} &__star-fill,
		&__label--delay#{$s} &__star-line,
		&__label--delay#{$s} &__star-stroke {
			animation-delay: 0.05s * $s;
		}
	}
	&__input {
		-webkit-appearance: none;
		appearance: none;
	}
	// display
	&__input:hover ~ [data-rating]:not([hidden]) {
		display: none;
	}

	&__input-1:hover ~ [data-rating="1"][hidden],
	&__input-2:hover ~ [data-rating="2"][hidden],
	&__input-3:hover ~ [data-rating="3"][hidden],
	&__input-4:hover ~ [data-rating="4"][hidden],
	&__input-5:hover ~ [data-rating="5"][hidden],
	&__input:checked:hover ~ [data-rating]:not([hidden]) {
		display: block;
	}
	// stars
	&__input-1:hover ~ &__label:first-of-type &__star-stroke,
	&__input-2:hover ~ &__label:nth-of-type(-n + 2) &__star-stroke,
	&__input-3:hover ~ &__label:nth-of-type(-n + 3) &__star-stroke,
	&__input-4:hover ~ &__label:nth-of-type(-n + 4) &__star-stroke,
	&__input-5:hover ~ &__label:nth-of-type(-n + 5) &__star-stroke {
		stroke: var(--yellow);
		transform: scale(1);
	}
	&__input-1:checked ~ &__label:first-of-type &__star-ring,
	&__input-2:checked ~ &__label:nth-of-type(-n + 2) &__star-ring,
	&__input-3:checked ~ &__label:nth-of-type(-n + 3) &__star-ring,
	&__input-4:checked ~ &__label:nth-of-type(-n + 4) &__star-ring,
	&__input-5:checked ~ &__label:nth-of-type(-n + 5) &__star-ring {
		animation-name: starRing;
	}
	&__input-1:checked ~ &__label:first-of-type &__star-stroke,
	&__input-2:checked ~ &__label:nth-of-type(-n + 2) &__star-stroke,
	&__input-3:checked ~ &__label:nth-of-type(-n + 3) &__star-stroke,
	&__input-4:checked ~ &__label:nth-of-type(-n + 4) &__star-stroke,
	&__input-5:checked ~ &__label:nth-of-type(-n + 5) &__star-stroke {
		animation-name: starStroke;
	}
	&__input-1:checked ~ &__label:first-of-type &__star-line,
	&__input-2:checked ~ &__label:nth-of-type(-n + 2) &__star-line,
	&__input-3:checked ~ &__label:nth-of-type(-n + 3) &__star-line,
	&__input-4:checked ~ &__label:nth-of-type(-n + 4) &__star-line,
	&__input-5:checked ~ &__label:nth-of-type(-n + 5) &__star-line {
		animation-name: starLine;
	}
	&__input-1:checked ~ &__label:first-of-type &__star-fill,
	&__input-2:checked ~ &__label:nth-of-type(-n + 2) &__star-fill,
	&__input-3:checked ~ &__label:nth-of-type(-n + 3) &__star-fill,
	&__input-4:checked ~ &__label:nth-of-type(-n + 4) &__star-fill,
	&__input-5:checked ~ &__label:nth-of-type(-n + 5) &__star-fill {
		animation-name: starFill;
	}
	&__input-1:not(:checked):hover ~ &__label:first-of-type &__star-fill,
	&__input-2:not(:checked):hover ~ &__label:nth-of-type(2) &__star-fill,
	&__input-3:not(:checked):hover ~ &__label:nth-of-type(3) &__star-fill,
	&__input-4:not(:checked):hover ~ &__label:nth-of-type(4) &__star-fill,
	&__input-5:not(:checked):hover ~ &__label:nth-of-type(5) &__star-fill {
		fill: var(--yellow-t);
	}
	// screen reader text
	&__sr {
		clip: rect(1px,1px,1px,1px);
		overflow: hidden;
		position: absolute;
		width: 1px;
		height: 1px;
	}
}

// Dark theme
@media (prefers-color-scheme: dark) {
	:root {
		--bg: #{hsl($hue,10%,10%)};
		--fg: #{hsl($hue,10%,90%)};
	}
	.rating {
		margin: auto;

		&__star {
			&-stroke {
				stroke: hsl($hue,10%,30%);
			}
		}
	}
}

// Animations
@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 12;
		stroke-dashoffset: -12;
	}
}

Javascript Code

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");
		});
	}
}

I hope you did find this tutorial useful!

For more web development or UI/UX design tutorials, follow us on:

Other useful resources:

Alexandra Murtaza

Alexandra Murtaza