Star Rating

Stars fill up to the one you hover, and the one you pick pops with a small bounce, all on native radio buttons.

  • CSS
  • Forms & controls
  • Hover
  • Added
Rate this motion

AI prompt

Create a five-star rating in pure CSS on native radio inputs. Wrap each input and an SVG star in a label, and lay the labels out with display: flex and flex-direction: row-reverse, ordered 5 to 1 in the DOM, so the sibling selector ~ reaches the stars visually to the left. Stars start with a muted fill. Fill the checked star and every star after it in the DOM with the accent colour using label:has(input:checked) and label:has(input:checked) ~ label. While the row is hovered, reset them and fill label:hover and label:hover ~ label instead. When a star is checked, play a 360ms keyframe on it: scale 1 to 1.35 to 1 with cubic-bezier(0.34, 1.56, 0.64, 1). Hide the inputs visually but keep them focusable, and show a focus ring on the label of the focused input. Give each input an aria-label like "4 stars". Under prefers-reduced-motion, skip the pop. The legend reads "Rate this motion".

Source

Demo.tsx
'use client'

import { useId } from 'react'
import { fmt } from '@/lib/i18n'
import s from './demo.module.css'

export default function StarRating({ labels }: { labels: Record<string, string> }) {
  const name = useId()

  return (
    <fieldset className={s.field}>
      <legend className={s.legend}>{labels.legend}</legend>
      <div className={s.stars}>
        {[5, 4, 3, 2, 1].map((n) => (
          <label key={n} className={s.star}>
            <input type="radio" name={name} value={n} className={s.input} aria-label={fmt(labels.star, n)} />
            <svg viewBox="0 0 24 24" aria-hidden="true">
              <path d="M12 2.8l2.8 5.9 6.4.8-4.7 4.4 1.2 6.4L12 17.2l-5.7 3.1 1.2-6.4-4.7-4.4 6.4-.8z" />
            </svg>
          </label>
        ))}
      </div>
    </fieldset>
  )
}
demo.module.css
.field {
  display: grid;
  justify-items: center;
  gap: 12px;
  margin: 0;
  padding: 0;
  border: 0;
}

.legend {
  margin-bottom: 12px;
  padding: 0;
  font: 500 var(--text-sm) / 1 var(--font-body);
  color: var(--stage-muted);
  text-align: center;
}

.stars {
  display: flex;
  flex-direction: row-reverse;
  gap: 6px;
}

.star {
  position: relative;
  display: grid;
  width: 36px;
  height: 36px;
  border-radius: 8px;
  cursor: pointer;
}

.star svg {
  width: 100%;
  height: 100%;
  fill: var(--stage-rule);
  transition: fill var(--dur-micro) var(--ease-out);
}

.input {
  position: absolute;
  inset: 0;
  opacity: 0;
  margin: 0;
  cursor: pointer;
}

.star:has(.input:checked) svg,
.star:has(.input:checked) ~ .star svg { fill: var(--stage-accent); }

.stars:hover .star svg { fill: var(--stage-rule); }
.stars:hover .star:hover svg,
.stars:hover .star:hover ~ .star svg { fill: var(--stage-accent); }

.star:has(.input:checked) svg { animation: pop 360ms cubic-bezier(0.34, 1.56, 0.64, 1); }
.star:has(.input:focus-visible) { outline: 2px solid var(--color-focus); outline-offset: 2px; }

@keyframes pop {
  50% { transform: scale(1.35); }
}

@media (prefers-reduced-motion: reduce) {
  .star:has(.input:checked) svg { animation: none; }
}

Suggest an idea

Seen a motion or a theme you would like to find here? Describe it, and it opens as a prefilled GitHub issue for you to post.

What are you suggesting?

Needs a GitHub account. Suggestions are public.