I currently have a rating system that produces 5 stars that can show half values. I did this by using FontAwesome's Half Stars and did some CSS tricks to make them look like one star. But I was thinking of increasing my React and CSS knowledge by coming up with a way to show only half of an SVG icon. So instead of using the Half Stars, I could use whatever Icon the user wanted, and it would only show 50% of the Icon for example if you wanted to give a 3.5 rating.
Q: Can you show only half of an Icon and somehow know if the user is clicking on one side or the other?
Here is the code I have currently that uses the HalfStars for a little bit of reference
import React, { useState } from 'react'
import { FaRegStarHalf, FaStarHalf } from 'react-icons/fa'
import './styles/Rater.css'
const Rater = ({ iconCount }) => {
const [rating, setRating] = useState(null)
const [hover, setHover] = useState(null)
// check if user has set a rating by clicking a star and use rating to determine icons
const Star = rating ? FaStarHalf : FaRegStarHalf
return (
<div>
{[...Array(iconCount), ...Array(iconCount)].map((icon, i) => {
const value = (i + 1) / 2
return (
<label>
<input
type='radio'
name='rating'
value={value}
onClick={() => {
console.log(`value => `, value)
return setRating(value)
}}
/>
<div className='star-container'>
<div>
<Star
className={i % 2 ? 'star-left' : 'star'}
color={value <= (hover || rating) ? '#ffc107' : '#e4e5e9'}
onMouseEnter={() => setHover(value)}
onMouseLeave={() => setHover(null)}
/>
</div>
</div>
</label>
)
})}
</div>
)
}
export default Rater
I have written a code to get you the idea; If you click on the right side of the star, its color changes to blue and if you click on the left side, its color changes to gold. Also, it's better to not use stopPropagation
and check e.target
of the event.
const starIcon = document.getElementById("star");
const icon = document.getElementById("icon");
starIcon.onclick = e => {
starIcon.style.color = "blue";
e.stopPropagation();
};
icon.onclick = e => {
starIcon.style.color = "gold";
}
i {
clip-path: inset(0 0 0 50%);
color: gold;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css" rel="stylesheet">
</head>
<body>
<span id="icon"><i id="star", class="fas fa-star"></i></span>
</body>
</html>
You can do it in ONE SVG
cl-social-star-solid
)M0 0h100v100h-100v-100
to the path0 0 300 100
viewBox to fit 3 stars.. see belowwidth="50%"
<svg viewBox="0 0 300 100" width="500px">
<rect id="rating" width="50%" fill="gold" height="100%" />
<path id="star" fill="green"
d="M0 0h100v100h-100v-100m91 42a6 6 90 00-4-10l-22-1a1 1 90 01-1
0l-8-21a6 6 90 00-11 0l-8 21a1 1 90 01-1 1l-22 1a6 6 90 00-4
10l18 14a1 1 90 010 1l-6 22a6 6 90 008 6l19-13a1 1 90 011 0l19
13a6 6 90 006 0a6 6 90 002-6l-6-22a1 1 90 010-1z"/>
<use href="#star" x="100" />
<use href="#star" x="200" />
<rect id="c" width="16.66%" height="100%" fill="transparent" stroke="red"
onclick="console.log(this)" />
<use href="#c" x="50" />
<use href="#c" x="100" />
<use href="#c" x="150" />
<use href="#c" x="200" />
<use href="#c" x="250" />
</svg>
<star-rating stars=N >
You don't want to create all this SVG by hand... couple lines of JavaScript can create the SVG, for any number of stars
Using a W3C standard Web Component here, because it runs in this page and is not as complex as a React Component.
https://developer.mozilla.org/en-US/docs/Web/Web_Components
<use>
, just duplicate all paths and rects with a x
offsetdocument.querySelector('[stars="5"]').rating="90%"
(4.5 stars)<star-rating stars=5 rating="3.5"
bgcolor="green" nocolor="grey" color="gold"></star-rating>
<star-rating stars=7 rating="50%"
bgcolor="rebeccapurple" nocolor="beige" color="goldenrod"></star-rating>
<script>
document.addEventListener("click", (evt) => console.log(evt.target.getAttribute("n")))
customElements.define("star-rating", class extends HTMLElement {
set rating( rate ) {
if (!String(rate).includes("%")) rate = Number(rate) / this.stars * 100 + "%";
this.querySelector("#rating").setAttribute("width", rate);
}
connectedCallback() {
let { bgcolor, stars, nocolor, color, rating } = this.attributes;
this.stars = ~~stars.value || 5;
this.innerHTML =
`<svg viewBox="0 0 ${this.stars*100} 100" style="cursor:pointer;width:300px">`
+ `<rect width="100%" height="100" fill="${nocolor.value}"/>`
+ `<rect id="rating" height="100" fill="${color.value}" />`
+ Array( this.stars ).fill()
.map((i, n) => `<path fill="${bgcolor.value}" d="M${ n*100 } 0h102v100h-102v-100m91 42a6 6 90 00-4-10l-22-1a1 1 90 01-1 0l-8-21a6 6 90 00-11 0l-8 21a1 1 90 01-1 1l-22 1a6 6 90 00-4 10l18 14a1 1 90 010 1l-6 22a6 6 90 008 6l19-13a1 1 90 011 0l19 13a6 6 90 006 0a6 6 90 002-6l-6-22a1 1 90 010-1z"/>`)
.join("")
+ Array( this.stars * 2 ).fill()
.map((i, n) => `<rect x="${ n*50 }" n="${n}" opacity="0" width="50" height="100"`
+ ` onclick="dispatchEvent(new Event('click'))" `
+ ` onmouseover="this.closest('star-rating').rating = ${(n+1)/2}"/>`)
.join("")
+ "</svg>";
this.rating = rating.value;
}
});
</script>
Notes:
<star-rating>
Component (also called Custom Element because NO shadowDOM is involved) has ZERO dependencies
<star-rating></star-rating>
M0 0h102v100h-102v-100
(2 pixel overlap) to cover SVG rounding issuesReact doesn't support this modern W3C Web Components Standard yet.
React scores just 71% on https://custom-elements-everywhere.com/
All other Frameworks (Angular, Vue, Svelte) have 100% support
You have to do some extra work to handle native DOM Elements and Events in React; but the Custom Element isn't complex.. it creates SVG; should be easy to replicate as a React Component.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With