Rating stars are a classic UI component used in most apps that implement an evaluating system. In this article, we're going to see how it could be done.
the following gif displays the final result of this tutorial, so keep reading π.
If you want to jump straight to the final code, you can skip all the explanation and get to the end of the article.
First, create the component folder in the src
folder, the component folder is going to hold our RatingStars.js
and the Star.js
files. for the CSS, we will use a style.css
file that will be imported in the App.js
component, the style.css
file resides in the src
folder and will contain all the CSS rules needed.
In the RatingStars
component, since we need 5 rating grades, I used an array to store those grades as strings like so:
const GRADES = ['Poor', 'Fair', 'Good', 'Very good', 'Excellent'];
In the RatingStars
component, I mapped through the GRADES
array to display a star for every value and I passed the index of each value as a prop to the Star
component. for the key
prop, I passed the grade.
As mentioned in the documentation:
Keys help React identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity.
If you want to read more about the key prop.
The h1
is meant to display the result of the review that is made by the user, it's going to stay empty for now.
We also added some CSS classes that we're going to write later.
import React from 'react';
import Star from './Star';
const RatingStars = () => {
const GRADES = ['Poor', 'Fair', 'Good', 'Very good', 'Excellent'];
return (
<div className="container">
<h1 className="result"></h1>
<div className="stars">
{
GRADES.map((grade, index) => (
<Star
index={index}
key={grade}
/>
))
}
</div>
</div>
);
}
export default RatingStars;
Now let's make the star
component, I used the star svg from iconsvg.
I associated every star with a radio button that has the index of the grade in the GRADES
array as a value and grouped the SVG element and the radio input element by the label element.
import React from 'react';
const Star = (props) => {
return (
<label className="star">
<input
type="radio"
name="rating"
id={props.grade}
value={props.index}
className="stars_radio-input"
/>
<svg
width="58"
height="58"
viewBox="0 0 24 24"
fill="none"
stroke="#393939"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
>
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
</svg>
</label>
);
}
export default Star;
So far, this is what our app looks like:
Now, it's time to make it prettier. inside the styles.css
file write the following classes:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
we're using the universal selector to reset the padding and the margin and also set the box-sizing as a border box which will help us size the elements. For more info about this property see the MDN web docs
.container {
padding: 16px;
margin: 16px auto;
}
The .container
class takes care of the spacing.
.result {
text-align: center;
margin-bottom: 16px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
The .result
class is applied to h1
element in the RatingStars
component.
.stars {
display: flex;
justify-content: center;
gap: 8px;
}
Concerning the stars class that wraps all the stars, we're using flex
value for the display properly, which will display the flex items (stars) horizontally, with a gap
of 8px between each star.
.star {
position: relative;
cursor: pointer;
}
For every star, we added a pointer cursor to insinuate to the user that the star is clickable. The position relative
is going to help us later position the radio button.
.stars_radio-input {
position: absolute;
top: 0;
left: 0;
width: 1px;
height: 1px;
clip: rect(1px, 1px, 1px, 1px);
}
This is going to visually hide the radio button.
.stars_radio-input:checked ~ svg {
fill: yellow;
}
This is a temporary class that we're going to use to just verify if the star is checked or not.
Since we're going to display the rating result in the RatingStar
component, we need to lift the state from the child component that is the Star.js
to the parent component that is the RatingStar.js
.
If you want to read more about lifting state: React Docs
To start making this work we need to declare a state in the parent component to store the grade's index:
const [gradeIndex, setGradeIndex] = useState();
This is the function used for setting the state in the parent component, and that we're going to pass it to the child component as a prop.
const changeGradeIndex = ( index ) => {
setGradeIndex(index);
}
And this the function that we're going to use in Star.js
component in order to update the state in the parent component.
const changeGrade = (e) => {
props.changeGrade(e.target.value);
}
Also, we'll attach an onClick
event on the radio button in Star.js
that will trigger the changeGrade
function.
onClick={changeGrade}
Inside the h1 in RatingStars.js
component, we used a ternary operator to display the value of the state only when the state is defined.
<h1 className="result">
{ GRADES[gradeIndex] ? GRADES[gradeIndex] : 'You didn\'t review yet'}
</h1>
Now, this is how our app looks like:
To make it behave more like the classic star rating UI component, we need to add the color yellow to the stars dynamically.
the activeStar
object is declared and assigned in the RatingStars
.
const activeStar = {
fill: 'yellow'
};
Then, pass it as a prop to the Star.js
, we also used a ternary operator here because we only want the clicked star along the previous stars starting from the left to have the color yellow.
style={ gradeIndex > index ? activeStar : {}}
Add the style attribute to the svg element.
<svg
width="58"
height="58"
viewBox="0 0 24 24"
fill="none"
stroke="#393939"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
style={props.style}
>
Abd don't forget to delete the .stars_radio-input:checked ~ svg
from the style.css
, since we don't need it anymore.
Finally, this is the whole code:
src\components\RatingStars.js
import React, { useState } from 'react';
import Star from './Star';
const RatingStars = () => {
const [gradeIndex, setGradeIndex] = useState();
const GRADES = ['Poor', 'Fair', 'Good', 'Very good', 'Excellent'];
const activeStar = {
fill: 'yellow'
};
const changeGradeIndex = ( index ) => {
setGradeIndex(index);
}
return (
<div className="container">
<h1 className="result">{ GRADES[gradeIndex] ? GRADES[gradeIndex] : 'You didn\'t review yet'}</h1>
<div className="stars">
{
GRADES.map((grade, index) => (
<Star
index={index}
key={grade}
changeGradeIndex={changeGradeIndex}
style={ gradeIndex > index ? activeStar : {}}
/>
))
}
</div>
</div>
);
}
export default RatingStars;
src\components\Star.js
import React from 'react';
const Star = (props) => {
const changeGrade = (e) => {
props.changeGradeIndex(e.target.value);
}
return (
<label className="star">
<input
type="radio"
name="rating"
id={props.grade}
value={props.index}
className="stars_radio-input"
onClick={changeGrade}
/>
<svg
width="58"
height="58"
viewBox="0 0 24 24"
fill="none"
stroke="#393939"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
style={props.style}
>
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
</svg>
</label>
);
}
export default Star;
src\style.css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.container {
padding: 16px;
margin: 16px auto;
}
.result {
text-align: center;
margin-bottom: 16px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.stars {
display: flex;
justify-content: center;
gap: 8px;
}
.star {
position: relative;
cursor: pointer;
}
.stars_radio-input {
position: absolute;
top: 0;
left: 0;
width: 1px;
height: 1px;
clip: rect(1px, 1px, 1px, 1px);
}
Top comments (2)
keep going yossra, best wishes :D
Thank you! :D