Inputs

Rating

A star rating that is both an accessible input (whole or half steps) and a fractional read-only display.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import { Star } from "lucide-react"
import { cn } from "@/lib/utils"

const SIZES = {
  sm: "size-4",
  md: "size-5",
  lg: "size-7",
} as const

export interface RatingProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
  value: number

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/rating.json

Prompt

The prompt behind this component — paste it into your AI assistant to recreate or adapt it.

Build a React + TypeScript + Tailwind "Rating" component (lucide-react Star).

Contract
- Export a forwardRef div extending HTMLAttributes (omit native onChange).
- Props: value (number), onChange? (value => void), max (default 5),
  allowHalf (default false), size ("sm" | "md" | "lg", default "md"),
  label (aria name, default "Rating").
- Mode is derived, not configured: onChange present → interactive input;
  absent → read-only display that supports fractional values like 3.7.

Behavior
- Every star renders from one primitive: an outline Star underneath and a
  filled Star inside an overflow-hidden overlay whose width is
  fraction × 100% — the same math draws hover previews, half steps and
  read-only decimals.
- Interactive: hover previews the prospective value (mousemove; left half of
  a star = x.5 when allowHalf), mouseleave reverts to the committed value,
  click commits via onChange.
- Keyboard (WAI-ARIA radiogroup): the container listens for
  ArrowRight/Up (+step), ArrowLeft/Down (−step), Home (min step), End (max);
  step is 0.5 when allowHalf. Clamp to [step, max], call onChange, and move
  focus to the star holding the new value.
- Roving tabindex: only the star matching ceil(value) is tabbable
  (the first when value is 0); every star is a role="radio" button with
  aria-checked and an "N of max" aria-label.

Rendering & styling
- Semantic tokens only: filled stars fill-primary text-primary, empty
  outlines text-muted-foreground/40 — the stars adopt any host theme.
- Read-only mode is role="img" with aria-label "value out of max"
  (one summary, not five radios).
- Hover scale (hover:scale-110) is disabled under motion-reduce; focus uses
  the focus-visible ring tokens. Merge className via cn().

Customization levers
- Scale: the SIZES map (size-4/5/7) — add an "xl" entry for hero placements.
- Color: swap primary for another token (e.g. fill-chart-4) when stars
  should not carry the brand accent; keep fill and text paired.
- Icon: replace Star with Heart / ThumbsUp from lucide — the fill-overlay
  math is icon-agnostic.
- Count: max is free (3, 5, 10); keyboard and fill logic follow it.
- Density: gap-0.5 on the container controls how tight the row reads.

Concepts

  • Input / display duality — the presence of onChange picks the mode, so a form input and a review-average display share one component and one visual language.
  • Width-clipped fill — a filled star inside an overflow-hidden span at fraction × 100% width renders whole stars, half steps and decimals like 3.7 with the same three lines of math.
  • Radiogroup pattern — stars are role="radio" buttons inside a labelled radiogroup, matching the WAI-ARIA rating idiom that screen readers announce as "3 of 5, selected".
  • Roving tabindex — one Tab stop for the whole control; arrows move the value and focus together, so keyboard users aren't tabbing through five buttons.
  • Hover preview vs committed value — mouse position previews a prospective rating that evaporates on mouseleave; only click commits, mirroring how users expect star widgets to behave.

On This Page