A native radio-group star rating with keyboard arrows, value text, and a clear action.
How useful was this setup guide?
import { Rating } from "@/components/ballmac/rating"
export default function RatingDemo() {
return (
<div className="w-full max-w-sm rounded-xl border bg-card p-5 shadow-sm">
<p className="mb-4 text-sm text-muted-foreground">
How useful was this setup guide?
</p>
<Rating label="Rate this guide" defaultValue={4} />
</div>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/ratingInstall the dependencies.
$ pnpm add lucide-reactAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/i18nCopy the source into your project.
components/ballmac/rating.tsx// Ballmac UI: Rating. https://ui.ballmac.com/components/rating "use client" import * as React from "react" import { Star } from "lucide-react" import { cn } from "@/lib/utils" import { useMessages } from "@/lib/ballmac/i18n" type RatingProps = Omit< React.ComponentProps<"fieldset">, "onChange" | "defaultValue" > & { /** Controlled rating. Zero means unrated. */ value?: number /** Initial rating when uncontrolled. */ defaultValue?: number /** Called when the rating changes. */ onValueChange?: (value: number) => void /** Highest rating. */ max?: number /** Visible and accessible label. */ label?: string /** Native radio group name. */ name?: string /** Show the current value beside the stars. */ showValue?: boolean /** Disable changes while keeping the rating visible. */ readOnly?: boolean } function Rating({ value, defaultValue = 0, onValueChange, max = 5, label, name, showValue = true, readOnly = false, disabled = false, className, ...props }: RatingProps) { const msg = useMessages() label ??= msg("rating.label", "Rating") const [internal, setInternal] = React.useState(defaultValue) const [hovered, setHovered] = React.useState(0) const current = value ?? internal const groupName = name ?? React.useId() const count = Math.max(1, Math.min(10, Math.floor(max))) function commit(next: number) { if (value === undefined) setInternal(next) onValueChange?.(next) } return ( <fieldset data-slot="rating" disabled={disabled} className={cn("min-w-0", className)} {...props} > <legend className="mb-1.5 text-sm font-medium">{label}</legend> <div className="flex flex-wrap items-center gap-1" onMouseLeave={() => setHovered(0)} > {Array.from({ length: count }, (_, index) => index + 1).map( (number) => ( <label key={number} data-slot="rating-item" className={cn( "relative flex size-9 items-center justify-center rounded-md text-muted-foreground transition-colors duration-150 motion-reduce:transition-none", !readOnly && !disabled && "cursor-pointer hover:bg-accent", (hovered || current) >= number && "text-chart-3", readOnly && "pointer-events-none", )} onMouseEnter={() => setHovered(readOnly ? 0 : number)} > <input type="radio" name={groupName} value={number} checked={current === number} disabled={disabled || readOnly} aria-label={msg("rating.ofStars", "{number} of {count} stars", { number, count })} onChange={() => { if (!readOnly) commit(number) }} className="peer sr-only" /> <Star aria-hidden="true" className={cn( "size-5 peer-focus-visible:drop-shadow-md", (hovered || current) >= number && "fill-current", )} /> <span aria-hidden="true" className="pointer-events-none absolute inset-0 rounded-md peer-focus-visible:ring-[3px] peer-focus-visible:ring-ring/50" /> </label> ), )} {showValue && ( <span data-slot="rating-value" className="ms-2 text-sm text-muted-foreground tabular-nums" > {current ? `${current} / ${count}` : "Not rated"} </span> )} {!readOnly && current > 0 && ( <button type="button" disabled={disabled} onClick={() => commit(0)} className="ms-1 rounded-md px-2 py-1 text-xs text-muted-foreground outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-50" > {msg("rating.clear", "Clear")} </button> )} </div> </fieldset> ) } export { Rating, type RatingProps }Update the import paths to match your project setup.
Usage
import { Rating } from "@/components/ballmac/rating"The full example is in the Code tab above.
Examples
States and variants
import { Rating } from "@/components/ballmac/rating"
export default function RatingStates() {
return (
<div className="w-full max-w-sm">
<Rating label="Rate your experience" max={5} showValue={false} />
</div>
)
}API reference
| Prop | Type | Default |
|---|---|---|
valueControlled rating. Zero means unrated. | number | — |
defaultValueInitial rating when uncontrolled. | number | 0 |
onValueChangeCalled when the rating changes. | (value: number) => void | — |
maxHighest rating. | number | 5 |
labelVisible and accessible label. | string | — |
nameNative radio group name. | string | — |
showValueShow the current value beside the stars. | boolean | true |
readOnlyDisable changes while keeping the rating visible. | boolean | false |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Arrow Left / Right | Changes the selected native radio |
| Tab / Space | Focuses and activates Clear |
Use with AI
A native radio-group star rating with keyboard arrows, value text, and a clear action. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Rating (@ballmac/rating) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Collect a quick score or satisfaction rating
- Display a read-only score beside content
Not for
- Use a text field for detailed feedback
Registry JSON: https://ui.ballmac.com/r/rating.json
Credits
Free to use in personal and commercial projects.
- npm
- lucide-react
- Registry
- @ballmac/i18nshadcn/utils
Pairs well with
Button
A button with six variants, three sizes, a pill shape and a built-in loading state. buttonVariants() styles links the same way.
Calendar
A date grid for one day, several days or a range, with month and year selects, week numbers, disabled rules and range-end styling, on React DayPicker.
Color Picker
A native color chooser paired with an editable hex field and a live swatch.
Combobox
A searchable single-choice select with groups, descriptions, keywords, clearable value, invalid state and hidden-input form submission.