Fooderia, the vibrant, dynamic recipe portal that you work for, is looking at launching a star rating component on their website to cultivate a more dynamic and interactive community experience. This addition is aimed at benefiting both users and recipe contributors. By allowing users to rate recipes from one to five stars, they're providing a simple yet powerful tool for feedback and engagement. This will not only help other users make informed decisions based on community preferences but also offer valuable insights to the recipe contributors, empowering them to refine and tailor their creations based on community feedback. The star rating system is a step towards making Fooderia a more user-centric platform, where the quality of content is continuously uplifted through active community participation.
Tutorial
A star rater, right? You have seen them everywhere, it lets you grade something by chosing a star in a range of stars, and generally the stars up and including the one you have chosen gets highlighted. You get assigned the task of creating the UI for this component, what an honor! Well, nothing else to do than to get on with it. You start by creating the necessary files to be able to implement and showcase the component:
Wow, it almost feels like you're done already! If it were solely a gadget to display ratings, you might have finished. However, this component is also expected to collect user input and give the application a chance to process the submitted rating, which is crucial for its operation. Currently, the component is highly visual without alternative methods for all users to understand the displayed rating.
After some thought, you figure that a range input element closely aligns with the input dynamics required by a star-rating system, due to its semantic appropriateness and inherent accessibility benefits. You outline different rating states: Zero stars indicating an unrated item, and one to five stars for rated items. The decision to use the input element assures compatibility with accessibility standards and browser functionality. Your intention is to visually replace this input with a graphical star system, enhancing user engagement without compromising accessibility. This approach ensures the component is both appealing and adheres to inclusive design principles, including keyboard and screen reader support.
Ok, enough with the rambling, you begin with adding the visual representation of the component:
components/StarRating/StarRating.jsx
import { useState } from 'react'
const FILLED = '★'
const UNFILLED = '☆'
// give the input an initial value
export default function StarRating ({ initValue = 0 }) {
const [rating, setRating] = useState (initValue)
return (
<ol className="flex gap-2">
{/*
Array (5) - creates an empty array with the length 5: you
cannot iterate over it
[...Array (5)] - results in an array with 5 'undefined'
items: you can iterate over it
For each possible rating level create a list item that
is either FILLED or UNFILLED depending on the current
rating value, and set the rating value when clicked on.
*/}
{[...Array (5)].map ((_, index) => {
return (
<li
key={index}
// cursor-pointer: Change the mouse cursor to indicate
// that the element is clickable.
// active:opacity-70: When the element is clicked,
// reduce the opacity.
className="cursor-pointer active:opacity-70"
onClick={() => setRating (index + 1)}
>{index < rating ? FILLED : UNFILLED}</li>
)
})}
</ol>
)
}
Before trying it out, you need to make sure that the component is exported as a client component (since you are using useState):
Improvement! However, after inspecting the component in the browser, you see that the stars container is very wide, you rather not have it take up more space than the stars themselves. There is also a gap between making the mouse pointer flicker when moving between the stars, you want to fix that as well. You also take the opportunity to prepare for the upcomint input element that you are planning for:
components/StarRating/StarRating.jsx
// ...
export default function StarRating ({ initRate = 0 }) {
const [rating, setRating] = useState (initRate)
// Prepare the component for the input element and wrap
// the list in a div! Make the div a flex container and
// center the items horizontally and vertically. Also
// make the stars bigger by increasing the font size:
Hm, not big of a difference but well worth it. A feature you are looking for is that when a user hovers over the component, deciding what the rating should be, the stars would be highlighted to match the star that is currently hovered. So if a user hovers over the fifth star, all stars would be highlighted, but if the user lingers over the first star only that star would be highlighted, no matter the actual current rating. You realise that you have to create a temporal universe here when the mouse is over the stars. That is you need a temporary state that is used when the mouse pointer enters the component and stop using it when the mouse pointer leaves it:
components/StarRating/StarRating.jsx
// ...
export default function StarRating ({ initRate = 0 }) {
// the current index + 1 (the rating the star represents),
// and when it leaves, set it back to the current rating.
onMouseEnter={() => setTmpRating (index + 1)}
onMouseLeave={() => setTmpRating (rating)}
// and use tmpRating instead of rating to fill the stars.
>{index < tmpRating ? FILLED : UNFILLED}</li>
)
})}
</ol>
</div>
)
}
Easy peasy! Ok, but you are eager to make the final touch: bolstering accessibility. Your plan involves leveraging a non-visible, yet functionally integral, range input element. This hidden interface allows the browser and assistive technologies to interact with and modify the rating, significantly enhancing component usability for all users. By connecting keyboard inputs directly to this element, users can adjust the rating more effortless:
components/StarRating/StarRating.jsx
// ...
export default function StarRating ({ initRate = 0 }) {
Hm, you are not sure it works, it looks just the same. But then you use the Tab key to navigate to the element. And, voilà, it focuses and you can even change the rating by using the arrow keys! Your star rater is set apart, from average into exceptional!
There is one more thing though, you have to be able to use whatever rating is set in parent components. You are just about to finish it up when the backend guys sends you a message. It turns out that they are using multiple ratings providers and those sometimes differ in the number of levels that are used, some use 10 and one even uses 7. They ask you to make sure the component can handle that. You answer back quickly - No problem guys, no problem at all! See you on friday.
Alright, you put, what you believe is the final touch on the component: support for arbitary levels and a way to use the rating being set, as a bonus you make it possible to set an element id to be able to associate labels with the component:
components/StarRating/StarRating.jsx
import { useEffect, useState } from 'react'
const FILLED = '★'
const UNFILLED = '☆'
// adding a levels and an onChange prop to be able to change
// the number of stars and provide callback to be called when
// the rating changes, and an id prop to be able to, for example,
// associate a label to the component
export default function StarRating ({ levels = 5, initRate = 0, onChange, id }) {
And there you have it, a fully functional, accessible star rating component. The team is impressed by your skills and eager to hand over the next task to you, it is about images...