AniUI Academy

Build a star rating

easyAccessibility+150 XP

Render five stars a person can rate something with, 1 through 5.

Hovering previews the rating (that star and every one before it lights up) without committing anything. Clicking commits the rating.

Examples

// Initial: rating = 0, no stars filled.

// Hover star 4
// → stars 1..4 get data-preview="true"; rating-value still "0".

// Click star 3
// → rating-value = "3"; stars 1..3 data-filled="true".

// Click star 5 → rating "5", 1..5 filled.
// Click star 2 → rating "2", only 1..2 filled (3..5 clear).

Preview and committed state are independent; re-rating down must clear higher stars.

const filled = n <= rating;              // committed
const preview = hovered !== null && n <= hovered; // transient

<button
  aria-label={`Rate ${n} star${n === 1 ? "" : "s"}`}
  data-filled={filled}
  data-preview={preview}
  onMouseEnter={() => setHovered(n)}
  onClick={() => setRating(n)}
/>

Two independent flags per star — the hover state never writes to the committed rating.

What it has to do

  • Each star is a `<button>` with `aria-label="Rate 1 star"` for the first star and `aria-label="Rate {n} stars"` for the rest.
  • Each star has `data-filled="true"` or `"false"` reflecting the committed rating, and `data-preview="true"` or `"false"` reflecting whichever star is currently hovered (and every star before it) — independent of the committed rating.
  • An element with `data-testid="rating-value"` shows the committed rating as plain text ("0" before anything is clicked).
  • Hovering must never change the committed rating shown in rating-value.
  • Re-rating (clicking a different star after one is already committed) must correctly update which stars are filled — both up and down.

Notes

  • Keep `rating` and `hovered` as separate state. Collapsing them into one "effective rating" during hover makes the committed value jump mid-preview.
  • Reset `hovered` to `null` on `onMouseLeave` of the row — otherwise the last-hovered star stays highlighted after the cursor leaves.
  • Keyboard support matters: each star is already a `<button>`, so Tab + Enter works. Add Left/Right arrow key support to rate without a mouse.
  • Give the group a label (`role="radiogroup"` + `aria-label="Rating"`) so a screen reader announces "Rating, 5 stars" rather than five anonymous buttons.
Asked atAirbnbZomatoSwiggyFlipkart

Stuck? The react course covers the ideas this problem is built on.

import { useState } from "react";

export default function App() {
  const [rating, setRating] = useState(0);
  const [hovered, setHovered] = useState(null);

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16 }}>
      <div style={{ display: "flex", gap: 4 }}>
        {[1, 2, 3, 4, 5].map((n) => {
          // TODO: `filled` should reflect the committed rating, and
          // `preview` should reflect the hovered star (and everything
          // before it), independent of the committed rating.
          const filled = false;
          const preview = false;

          return (
            <button
              key={n}
              aria-label={`Rate ${n} star${n === 1 ? "" : "s"}`}
              data-filled={filled}
              data-preview={preview}
              onMouseEnter={() => setHovered(n)}
              onClick={() => setRating(n)}
              style={{
                fontSize: 24,
                border: "none",
                background: "none",
                cursor: "pointer",
                color: filled || preview ? "#f5a623" : "#ccc",
              }}
            >
              {"\u2605"}
            </button>
          );
        })}
      </div>
      <p>
        Rating: <span data-testid="rating-value">{rating}</span>
      </p>
    </div>
  );
}

5 tests · +150 XP

Discussion

Sign in to post
Sign in to join the conversation.Sign in

No comments yet. Be the first to say something.