The Idea Behind
I remember using an application sometime back and the review request popped up, it was just the plain 5 golden stars and a basic textarea. I simply canceled it and said to myself "Giving reviews is so boring, maybe if the review system looked more fun I would give reviews more often😅. I bet I could make something more interesting". Yep, that's how the idea for this project was born💡. Sheer laziness produced the need to create something more complex but at least more interesting.
Well the main point I had in mind when creating this project, was to showcase how making the star rating interface multicolored could enhance user experience, making it more visually appealing, which adds an element of excitement, and promotes more engagement for the rating process.
In this article, I'll walk you through how I created the multicolored star rating card component which also supports fractional values rating.
Below is a preview of what we will be creating:
Without further time wasting, let's dive in.
Setting up the HTML structure
To get started, we first include the header section with the project's title.
<header>
<span>☆</span>
<h1>Interactive Star Rating</h1>
<span>☆</span>
</header>
Next, inside the main
tag, we need to create a container div
and inside this div
, we include 2 other divs to hold the front and back sections of the card.
<main>
<div id="rating-card">
<div class="card front">
</div>
<div class="card back">
</div>
</div>
</main>
Inside the card front
div, we include the card heading as well as a div
container for the emojis and a p
element to display the rating texts.
<h2>Please Rate Us</h2>
<div id="emoticon-container"> </div>
<p id="satisfaction-text"> </p>
After that, in the card front
div, we create another div
that holds the rating stars, we'll be making use of HTML entities, for the star icons. We'll then create two sets of stars, filled and empty stars. In between these sets, we are going to include a blank slider, we will get to the purpose of this slider in a bit.
<div id="rating-component-container">
<div class="filled-stars">
<span>★</span>
<span>★</span>
<span>★</span>
<span>★</span>
<span>★</span>
</div>
<div id="blank-slider"></div>
<div class="empty-stars">
<span>☆</span>
<span>☆</span>
<span>☆</span>
<span>☆</span>
<span>☆</span>
</div>
</div>
The next thing we need to include in the card front is the input field that will allow for keyboard and fractional value ratings, as well as the button that will be used to switch the card to the back.
<input type="number" id="val" step="0.1" min="0" max="10" placeholder="1 - 10">
<button id="wr-btn">Write Review</button>
That's all for the front section of our card, now let's move on to creating the card back. Inside the card back
div, we first need to include an arrow Icon that will take us back to the front section of the card, and then include the information that will only be visible at the card back.
<span id="back-btn">⇦</span>
<h2>Write Review</h2>
<p>Please share your thoughts about our app, so we can serve you better.</p>
Next, we include a textarea that will allow the user to leave written reviews along with a button that submits the overall rating.
<textarea id="review-field" placeholder="Write a review..." ></textarea>
<button id="rate-us">Rate Us</button>
Now that we're done creating the rating card let's create one last section just before the main
closing tag, to thank the user after their review has been sent.
<div class="sent">
<h2> Review Sent! </h2>
<p>Thank you for rating us.</p>
<button id="go-back-btn">Go Back</button>
</div>
That's all for our HTML file, now let's move into our CSS file to style the elements.
Styling to the card with CSS
To get started with our styling, we first need to declare the color variables that we will make use of throughout the stylesheet, as well as add some general styles to the document.
:root {
--lightBlue: #43a6c6;
--darkBlue: #03000f;
--darkGray: #333;
--starSize: 3.5em;
}
html {
background-color: var(--darkBlue);
color: whitesmoke;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
Next, we style the header
section of the page along with the h1
element.
header {
display: flex;
justify-content: space-between;
padding: 25px;
letter-spacing: 2px;
position: sticky;
top: 0;
z-index: 2;
border-bottom: 2px solid;
background-color: var(--darkBlue);
text-shadow: 3px 3px 5px var(--lightBlue);
box-shadow: 10px 10px 20px -10px var(--lightBlue);
}
h1 {
font-size: 2.5em;
}
header > span {
font-size: 25px;
color: #ffee00;
}
Moving on, we style the main
content section, as well as the rating-card
container where we will set its transform-style
property to preserve-3d
. This ensures that the reversed front of the card is not visible when flipped and it only shows the card back.
main {
min-height: 90vh;
display: grid;
place-items: center;
perspective: 800px;
}
#rating-card {
position: relative;
box-shadow: 1px 1px 30px -1px var(--lightBlue);
border-radius: 20px;
transition: 2s;
min-width: 250px;
transform-style: preserve-3d; /* very important */
}
Next up, we apply general styling for both the front and back cards.
.card {
display: grid;
place-items: center;
padding: 25px 20px 40px;
background-color: var(--darkBlue);
border-radius: 20px;
}
What we need to do next is to set the backface-visibility
of the card front to hidden
to prevent it from reflecting when flipped.
.front {
backface-visibility: hidden;
}
After that, we style the h2
and p
tags on the card, along with the container for the expression emojis.
h2 {
text-shadow: 1px 1px 3px var(--lightBlue);
margin-bottom: 10px;
}
p {
padding: 10px;
color: #999;
}
#emoticon-container {
font-size: var(--starSize);
margin-bottom: 10px;
}
Next, we style the rating stars container in the card.
#rating-component-container {
position: relative;
overflow: hidden;
cursor: pointer;
user-select: none;
}
After that, we style the filled stars container, giving it a multicolored background image ranging from red to green, and clip the background image of the element to its text content, while setting the color of the filled stars in the container to transparent to reflect the background.
.filled-stars {
background-image: linear-gradient(to right, red 5%, yellow, #00ff00);
background-clip: text;
-webkit-background-clip: text;
transform: scale(1);
}
.filled-stars > span {
font-size: var(--starSize);
color: transparent;
}
Next up, we style the blank-slider
div, setting its width
property to 100%
and moving it to the absolute right of its container. When we get to the JavaScript section, you will understand why it's important that it's done this way.
#blank-slider {
background-color: var(--darkBlue);
position: absolute;
height: 100%;
right: 0; /*very important*/
top: 0;
width: 100%; /*very important*/
transition: 3s;
}
Style the empty-stars
container, as well as the empty stars span
elements.
.empty-stars {
position: absolute;
top: 0;
right: 0;
height: 100%;
width: 100%;
transform: scale(1);
transition: 1s;
}
.empty-stars > span {
color: var(--darkGray);
font-size: var(--starSize);
cursor: pointer;
}
After that, we style the value input field along with its hover state.
#val {
background-color: transparent;
color: aliceblue;
outline: none;
padding: 8px;
margin-top: 15px;
letter-spacing: 2px;
font-weight: 700;
border-radius: 10px;
border: 2px solid var(--darkGray);
width: 80%;
}
#val:hover {
border: 2px solid var(--lightBlue);
}
That's all for the front section of the card.
Next up, we style two individual classes which we'll apply to the cards through JavaScript.
.p-events {
pointer-events: none;
}
.flipped {
transform: rotateY(180deg);
}
Moving on, we get to styling the back view of the card, along with all it's contents.
.back {
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: -1;
transform: rotateY(180deg);
pointer-events: none;
}
#back-btn {
color: var(--lightBlue);
position: absolute;
top: 10px;
left: 12px;
font-weight: 900;
font-size: 25px;
cursor: pointer;
}
#back-btn:hover {
transform: scale(1.2);
}
.back textarea {
border-radius: 10px;
background-color: transparent;
padding: 10px;
height: 100px;
color: #ccc;
width: 90%;
outline: none;
border: 2px solid var(--darkGray);
resize: none;
letter-spacing: 1.5px;
overflow: hidden;
font-weight: 100;
font-size: 12px;
}
.back > textarea:hover, button:hover {
box-shadow: 1px 1px 20px -1px var(--lightBlue);
}
Now that we're done styling the rating card, let's style all the button elements in the document, along with the popup "Thank you" note that shows after the "rate-us" button is clicked.
button {
margin-top: 30px;
border-radius: 10px;
padding: 10px;
background-color: var(--lightBlue);
border: none;
font-size: 0.9em;
font-weight: 600;
}
.sent {
display: none;
place-items: center;
}
Well, that's all for our CSS. quite a lot for such a little card right?😅. Now let's add some functionality to the card.
Adding functionality with JavaScript
Inside our JavaScript file, we first need to get all the elements we'll be working with later on.
const blankSlider = document.getElementById("blank-slider");
const emptyStars = document.querySelector(".empty-stars");
const val = document.getElementById("val");
const wrBtn = document.getElementById("wr-btn");
const backBtn = document.getElementById("back-btn");
const cardFront = document.querySelector(".front");
const cardBack = document.querySelector(".back");
const ratingCard = document.getElementById("rating-card");
const satisfactionTxtContainer = document.getElementById("satisfaction-text");
const goBackBtn = document.getElementById("go-back-btn");
const sentInfo = document.querySelector(".sent");
const rateUsBtn = document.getElementById("rate-us");
const reviewField = document.getElementById("review-field");
const emoticonContainer = document.getElementById("emoticon-container");
Next up, we need to declare three arrays, one for holding the different colors of the stars, the second to hold the HTML character codes for the emojis, and the third to hold the different texts that will be displayed according to the user's rating.
const hoverColors = ['red', 'orange', 'yellow', 'chartreuse', 'lime'];
const emoticons = ['😡', '🙁', '😐', '😊', '🤩'];
const satisfactionTxt = ['Highly Unsatisfied', 'Slightly Unsatisfied', 'feeling Indifferent', 'Quite Satisfied', 'Extremely Satisfied'];
Calculating and displaying the stars' colors
Firstly, we need to create a simple function that will be used to reverse numbers. I would get to the importance of this function in a bit, stay with me.
// Function to reverse numbers within 100
function reverseValue(value) {
return 100 - value;
}
Next up, we need to loop through all the empty stars elements.
// loop through all the empty stars element
for (let i = 0; i < emptyStars.children.length; i++) {
// Stars color logic goes here.
}
Inside this loop, we change the color of the hovered star on mouseenter and restore the default color of hovered star on mouseleave.
emptyStars.children[i].onmouseenter = (e) => {
emptyStars.children[i].style.color = hoverColors[i];
};
emptyStars.children[i].onmouseleave = (e) => {
emptyStars.children[i].style.color = "#333";
};
Next up, we need to update the val
input value, blankSlider
, satisfactionText
, and emoticons
according to the empty star that was clicked. This way, the blankSlider
width moves forward or backward, covering or revealing the filled stars underneath the empty stars.
emptyStars.children[i].onclick = (e) => {
// changes the value of the input field to the number rating for the clicked star.
val.value = i * 2 + 2;
// sets the width percentage value of the slider to the index value of the clicked star plus 1 times 20 in reverse
blankSlider.style.width = `${reverseValue((i + 1) * 20)}%`;
satisfactionTxtContainer.innerHTML = satisfactionTxt[i];
emoticonContainer.innerHTML = emoticons[i];
};
}
Now, we want to be able to manipulate the slider movements as well as the display of the text and emoji's through the number input field alone, so we attach an input
event listener to the val
input field, which will also allow the slider respond to fractional value ratings.
// Add input event listener to the input value(val) element
val.addEventListener('input', (e) => {
// prevents negative values from being entered into the input field, setting the lowest possible value to 0
if (val.value < 0) {
val.value = 0;
}
// Update the slider width based on the input value
blankSlider.style.width = `${reverseValue(val.value * 10)}%`;
// Converts the input value to a number 4 or less to represent the index value to use for the different arrays.
const index = Math.min(Math.floor(val.value / 2 -0.5), 4);
// Updates the satisfaction text and emoticon based on the input value
satisfactionTxtContainer.innerHTML = index < 0 || val.value == "" ? ' ' : satisfactionTxt[index];
emoticonContainer.innerHTML = index < 0 || val.value == "" ? ' ' : emoticons[index];
})
Next, we apply the flipped
and p-events
classes which we created in CSS earlier to the ratingCard
and cardFront
respectively on click of the "write review" button, and remove them on click of the arrow backBtn
at the card back, which flips it back to the card front.
// Flips to the card back
wrBtn.onclick=()=>{
ratingCard.classList.add("flipped");
cardFront.classList.add("p-events");
cardBack.style.pointerEvents="visible";
}
// Flips to the card front
backBtn.onclick=()=>{
ratingCard.classList.remove("flipped");
cardFront.classList.remove("p-events")
cardBack.style.pointerEvents= "none";
}
After that, on click of the "rate us" button, we store the user's rating information in an object, which we can then send to our server, but in this case we just log the information to the console.
// user's rating info object.
let userRating = {
ratingValue: "",
satisfaction: "",
review: "",
};
// store the user's rating info
rateUsBtn.onclick = () => {
ratingCard.style.display = "none";
sentInfo.style.display = "grid";
userRating.ratingValue = val.value;
userRating.satisfaction = satisfactionTxtContainer.textContent;
userRating.review = reviewField.value;
console.log(userRating);
}
// Removes the thank you note and goes back to the rating card.
goBackBtn.onclick = () => {
ratingCard.style.display = "block";
sentInfo.style.display = "none";
}
With that, our multicolored star rating card is fully functional🤗.
Conclusion
That's all for this article. I hope you enjoyed following along guys. You can check out the complete implementation of this project in the codepen below:
Please don't forget to leave your comments on what you think about this, or if you have any questions or maybe need additional clarification about the implementation.
Also, if there is a tutorial you would like me to cover, please don't hesitate to leave it in the comment sections. Till then, happy coding!😊