Five real radios in natural order: :has() lights the chosen star and every star before it in a popping wave, so arrow keys move the right way. Hover previews a rating first.
Also known as: css star rating, 5 star rating css, star rating without javascript.
<div class="rad-star-rating" role="radiogroup" aria-label="Rating"> <label style="--i: 0"><input type="radio" name="rad-star-rating" value="1" aria-label="1 star"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2.8l2.8 5.7 6.2.9-4.5 4.4 1.1 6.2L12 17.1 6.4 20l1.1-6.2L3 9.4l6.2-.9z"/></svg></label> <label style="--i: 1"><input type="radio" name="rad-star-rating" value="2" aria-label="2 stars"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2.8l2.8 5.7 6.2.9-4.5 4.4 1.1 6.2L12 17.1 6.4 20l1.1-6.2L3 9.4l6.2-.9z"/></svg></label> <label style="--i: 2"><input type="radio" name="rad-star-rating" value="3" aria-label="3 stars" checked><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2.8l2.8 5.7 6.2.9-4.5 4.4 1.1 6.2L12 17.1 6.4 20l1.1-6.2L3 9.4l6.2-.9z"/></svg></label> <label style="--i: 3"><input type="radio" name="rad-star-rating" value="4" aria-label="4 stars"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2.8l2.8 5.7 6.2.9-4.5 4.4 1.1 6.2L12 17.1 6.4 20l1.1-6.2L3 9.4l6.2-.9z"/></svg></label> <label style="--i: 4"><input type="radio" name="rad-star-rating" value="5" aria-label="5 stars"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2.8l2.8 5.7 6.2.9-4.5 4.4 1.1 6.2L12 17.1 6.4 20l1.1-6.2L3 9.4l6.2-.9z"/></svg></label> </div>
.rad-star-rating {
--c: var(--rad-color, #3b82f6);
font-size: var(--rad-size, 18px);
display: inline-flex;
}
.rad-star-rating label {
position: relative;
display: block;
padding: 0 0.12em;
cursor: pointer;
}
.rad-star-rating input {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
z-index: 1;
}
.rad-star-rating svg {
display: block;
width: 2.2em;
height: 2.2em;
fill: currentColor;
stroke: currentColor;
stroke-width: 1.5;
stroke-linejoin: round;
color: color-mix(in srgb, currentColor 16%, transparent);
transition: color 0.25s ease, scale 0.25s ease;
}
.rad-star-rating label:has(:checked) svg,
.rad-star-rating label:has(~ label :checked) svg {
color: var(--c);
animation: rad-star-rating-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both;
animation-delay: calc(var(--i) * 70ms);
}
.rad-star-rating:hover label:nth-child(n) svg {
color: color-mix(in srgb, currentColor 16%, transparent);
}
.rad-star-rating:hover label:hover svg,
.rad-star-rating:hover label:has(~ label:hover) svg {
color: color-mix(in srgb, var(--c) 70%, transparent);
scale: 1.08;
}
.rad-star-rating input:focus-visible + svg {
outline: 0.1em solid var(--c);
outline-offset: 0.05em;
border-radius: 0.4em;
}
@keyframes rad-star-rating-pop {
0% { transform: scale(1); }
40% { transform: scale(1.3) rotate(-10deg); }
100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
.rad-star-rating, .rad-star-rating *, .rad-star-rating::before, .rad-star-rating::after, .rad-star-rating *::before, .rad-star-rating *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}--rad-color on the element or a parent.
Currently previewing #3b82f6.--rad-size. Every other dimension is in
em, so the whole thing scales together. Currently previewing 18px.transition or animation
shorthand. Staggered delays are relative, so they keep their rhythm.