TikTok Tutorial #23 - How to create an Animated Rating in CSS and JavaScript

Learn with us how to create an animated rating in 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 Add this animated rating in JS to your website 🤩 check the link in bio for full code #coding #techtok #programmingexercises #javascriptprogramming ♬ original sound - Creative Tim

Contents:
1. HTML Code
2. CSS Code
3. Javascript Code

Get your code ⬇️

1. HTML Code

<form class="fr" action="">
	<label class="fr__label" for="face-rating">Rate your experience with us</label>
	<div class="fr__face" role="img" aria-label="Straight face">
		<div class="fr__face-right-eye" data-right></div>
		<div class="fr__face-left-eye" data-left></div>
		<div class="fr__face-mouth-lower" data-mouth-lower></div>
		<div class="fr__face-mouth-upper" data-mouth-upper></div>
	</div>
	<input class="fr__input" id="face-rating" type="range" value="2.5" min="0" max="5" step="0.1">
</form>

2. CSS Code

* {
	border: 0;
	box-sizing: border-box;
	margin: 0;
	padding: 0;
}
:root {
	--hue: 223;
	--white: hsl(var(--hue),10%,100%);
	--lt-gray: hsl(var(--hue),10%,95%);
	--gray1: hsl(var(--hue),10%,90%);
	--gray2: hsl(var(--hue),10%,80%);
	--gray7: hsl(var(--hue),10%,30%);
	--gray9: hsl(var(--hue),10%,10%);
	--primary: hsl(var(--hue),90%,55%);
	--trans-dur: 0.3s;
	font-size: calc(16px + (24 - 16) * (100vw - 320px) / (1280 - 320));
}
body,
input {
	font: 1em/1.5 "DM Sans", sans-serif;
}
body {
	background-color: var(--white);
	color: var(--gray9);
	height: 100vh;
	display: grid;
	place-items: center;
	transition:
		background-color var(--trans-dur),
		color var(--trans-dur);
}

/* Main styles */
.fr {
	animation: fade-slide-in 0.6s ease-out;
	padding: 0 1.5em;
	max-width: 20em;
}
.fr__face {
	--face-hue1: 60;
	--face-hue2: 30;
	background-image:
		linear-gradient(
			135deg,
			hsl(var(--face-hue1),90%,55%),
			hsl(var(--face-hue2),90%,45%)
		);
	border-radius: 50%;
	box-shadow: 0 0.5em 0.75em hsla(var(--face-hue2),90%,55%,0.3);
	margin: 0 auto 2em;
	position: relative;
	width: 3em;
	height: 3em;
}
.fr__face-right-eye,
.fr__face-left-eye,
.fr__face-mouth-lower,
.fr__face-mouth-upper {
	position: absolute;
	transition:
		background-color var(--trans-dur),
		box-shadow var(--trans-dur),
		color var(--trans-dur);
}
.fr__face-right-eye,
.fr__face-left-eye {
	background-color: var(--white);
	border-radius: 50%;
	top: 0.75em;
	width: 0.6em;
	height: 0.6em;
}
.fr__face-right-eye {
	--delay-right: 0s;
	animation: right-eye 1s var(--delay-right) linear paused;
	clip-path: polygon(0 75%,100% 0,100% 100%,0 100%);
	left: 0.6em;
}
.fr__face-left-eye {
	--delay-left: 0s;
	animation: left-eye 1s var(--delay-left) linear paused;
	clip-path: polygon(0 0,100% 75%,100% 100%,0 100%);
	right: 0.6em;
}
.fr__face-mouth-lower,
.fr__face-mouth-upper {
	color: var(--white);
	top: 1.75em;
	left: 0.75em;
	width: 1.5em;
	height: 0.75em;
}
.fr__face-mouth-lower {
	--delay-mouth-lower: 0s;
	animation: mouth-lower 1s var(--delay-mouth-lower) linear paused;
	border-radius: 50% 50% 0 0 / 100% 100% 0 0;
	box-shadow: 0 0.125em 0 inset;
}
.fr__face-mouth-upper {
	--delay-mouth-upper: 0s;
	animation: mouth-upper 1s var(--delay-mouth-upper) linear paused;
	border-radius: 0 0 50% 50% / 0 0 100% 100%;
	box-shadow: 0 -0.125em 0 inset;
}
.fr__label {
	display: block;
	margin-bottom: 1.5em;
	text-align: center;
}
.fr__input {
	--input-hue: 60;
	--percent: 50%;
	background-color: var(--gray1);
	background-image: linear-gradient(hsl(var(--input-hue),90%,45%),hsl(var(--input-hue),90%,45%));
	background-size: var(--percent) 100%;
	background-repeat: no-repeat;
	border-radius: 0.25em;
	display: block;
	margin: 0.5em auto;
	width: 100%;
	max-width: 10em;
	height: 0.5em;
	transition: background-color var(--trans-dur);
	-webkit-appearance: none;
	appearance: none;
	-webkit-tap-highlight-color: transparent;
}
.fr__input:focus {
	outline: transparent;
}

/* WebKit */
.fr__input::-webkit-slider-thumb {
	background-color: var(--white);
	border: 0;
	border-radius: 50%;
	box-shadow: 0 0.125em 0.5em hsl(0,0%,0%,0.3);
	width: 1.5em;
	height: 1.5em;
	transition: background-color 0.15s linear;
	-webkit-appearance: none;
	appearance: none;
}
.fr__input:focus::-webkit-slider-thumb,
.fr__input::-webkit-slider-thumb:hover {
	background-color: var(--lt-gray);
}

/* Firefox */
.fr__input::-moz-range-thumb {
	background-color: var(--white);
	border: 0;
	border-radius: 50%;
	box-shadow: 0 0.125em 0.5em hsl(0,0%,0%,0.3);
	width: 1.5em;
	height: 1.5em;
	transition: background-color 0.15s linear;
}
.fr__input:focus::-moz-range-thumb,
.fr__input::-moz-range-thumb:hover {
	background-color: var(--lt-gray);
}

/* `:focus-visible` support */
@supports selector(:focus-visible) {
	.fr__input:focus::-webkit-slider-thumb {
		background-color: var(--white);
	}
	.fr__input:focus-visible::-webkit-slider-thumb,
	.fr__input::-webkit-slider-thumb:hover {
		background-color: var(--lt-gray);
	}
	.fr__input:focus::-moz-range-thumb {
		background-color: var(--white);
	}
	.fr__input:focus-visible::-moz-range-thumb,
	.fr__input::-moz-range-thumb:hover {
		background-color: var(--lt-gray);
	}
}

/* Dark theme */
@media (prefers-color-scheme: dark) {
	body {
		background-color: var(--gray9);
		color: var(--gray1);
	}
	.fr__face-right-eye,
	.fr__face-left-eye {
		background-color: var(--gray9);
	}
	.fr__face-mouth-lower,
	.fr__face-mouth-upper {
		color: var(--gray9);
	}
	.fr__input {
		background-color: var(--gray7);
	}
}

/* Animations */
@keyframes fade-slide-in {
	from,
	16.67% {
		opacity: 0;
		transform: translateY(25%);
	}
	to {
		opacity: 1;
		transform: translateY(0);
	}
}
@keyframes right-eye {
	from { clip-path: polygon(0 75%,100% 0,100% 100%,0 100%); }
	50%, to { clip-path: polygon(0 0,100% 0,100% 100%,0 100%); }
}
@keyframes left-eye {
	from { clip-path: polygon(0 0,100% 75%,100% 100%,0 100%); }
	50%, to { clip-path: polygon(0 0,100% 0,100% 100%,0 100%); }
}
@keyframes mouth-lower {
	from {
		border-radius: 50% 50% 0 0 / 100% 100% 0 0;
		top: 1.75em;
		height: 0.75em;
		visibility: visible;
	}
	40% {
		border-radius: 50% 50% 0 0 / 100% 100% 0 0;
		top: 1.95em;
		height: 0.25em;
		visibility: visible;
	}
	50%,
	to {
		border-radius: 0;
		top: 2em;
		height: 0.125em;
		visibility: hidden;
	}
}
@keyframes mouth-upper {
	from,
	50% {
		border-radius: 0;
		box-shadow: 0 -0.125em 0 inset;
		top: 2em;
		height: 0.125em;
		visibility: hidden;
	}
	62.5% {
		border-radius: 0 0 50% 50% / 0 0 100% 100%;
		box-shadow: 0 -0.125em 0 inset;
		top: 1.95em;
		height: 0.25em;
		visibility: visible;
	}
	75% {
		border-radius: 0 0 50% 50% / 0 0 100% 100%;
		box-shadow: 0 -0.125em 0 inset;
		top: 1.825em;
		height: 0.5em;
		visibility: visible;
	}
	to {
		border-radius: 0 0 50% 50% / 0 0 100% 100%;
		box-shadow: 0 -0.8em 0 inset;
		top: 1.75em;
		height: 0.75em;
		visibility: visible;
	}
}

3. Javascript Code

window.addEventListener("DOMContentLoaded",() => {
	const fr = new FaceRating("#face-rating");
});

class FaceRating {
	constructor(qs) {
		this.input = document.querySelector(qs);
		this.input?.addEventListener("input",this.update.bind(this));
		this.face = this.input?.previousElementSibling;
		this.update();
	}
	update(e) {
		let value = this.input.defaultValue;

		// when manually set
		if (e) value = e.target?.value;
		// when initiated
		else this.input.value = value;

		const min = this.input.min || 0;
		const max = this.input.max || 100;
		const percentRaw = (value - min) / (max - min) * 100;
		const percent = +percentRaw.toFixed(2);

		this.input?.style.setProperty("--percent",`${percent}%`);

		// face and range fill colors
		const maxHue = 120;
		const hueExtend = 30;
		const hue = Math.round(maxHue * percent / 100);

		let hue2 = hue - hueExtend;
		if (hue2 < 0) hue2 += 360;

		const hues = [hue,hue2];
		hues.forEach((h,i) => {
			this.face?.style.setProperty(`--face-hue${i + 1}`,h);
		});

		this.input?.style.setProperty("--input-hue",hue);

		// emotions
		const duration = 1;
		const delay = -(duration * 0.99) * percent / 100;
		const parts = ["right","left","mouth-lower","mouth-upper"];

		parts.forEach(p => {
			const el = this.face?.querySelector(`[data-${p}]`);
			el?.style.setProperty(`--delay-${p}`,`${delay}s`);
		});

		// aria label
		const faces = [
			"Sad face",
			"Slightly sad face",
			"Straight face",
			"Slightly happy face",
			"Happy face"
		];
		let faceIndex = Math.floor(faces.length * percent / 100);
		if (faceIndex === faces.length) --faceIndex;

		this.face?.setAttribute("aria-label",faces[faceIndex]);
	}
}

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