Loading…
Loading…
Allows users to rate or view ratings using a visual scale (typically stars).
The Rating component allows users to provide or view evaluative feedback on a visual scale — most commonly rendered as a row of stars (★). Ratings are ubiquitous in e-commerce (product reviews), content platforms (movie/music ratings), service marketplaces (driver/host ratings), and internal tools (feedback forms, satisfaction surveys).
Despite their apparent simplicity, ratings involve nuanced design decisions: Should users be able to select half-stars? Should hovering preview the rating before committing? How does a read-only aggregate rating (4.3 out of 5) differ visually from an interactive input rating? What happens on mobile where hover doesn't exist? And critically, how does a screen reader user interact with what is essentially a row of icon buttons?
When to use a Rating:
When NOT to use a Rating:
Preview star fill animations with the Animation Tool, and verify that star colors meet contrast requirements using the Contrast Checker.
| Variant | Purpose | Visual Treatment |
|---|---|---|
| Interactive | User input — click/tap to set a rating. | Filled stars up to the selected value, empty stars beyond. Hover preview. |
| Read-only | Display an existing rating. | Fractional fill (e.g., 4.3 stars = 4 full + 1 at 30%). No interaction. |
| Compact | Space-constrained contexts (table cells, cards). | Single star icon + numeric label (★ 4.3). |
| Emoji | Sentiment scale (1=😡, 5=😍). | Replaces stars with emoji or face icons. Common for CSAT surveys. |
| Heart | Affection-based ratings (favorites, love). | Uses ♥ instead of ★. Common on social/content platforms. |
| Custom icon | Domain-specific scales. | Any icon — thumbs up, flame, diamond, etc. |
| Precision | Description | Use Case |
|---|---|---|
| Full (1.0) | Only whole values (1, 2, 3, 4, 5). | Simple feedback, mobile-first UIs. |
| Half (0.5) | Allows half-star increments (3.5, 4.0, 4.5). | Product reviews with moderate precision. |
| Quarter (0.25) | Finer granularity for display (4.25). | Read-only aggregate displays. Rarely used for input. |
| Exact (any) | Continuous value for display (4.37). | Read-only. Uses CSS clip or gradient for partial fill. |
| Size | Icon Size | Gap | Use Case |
|---|---|---|---|
| Small (sm) | 16px | 2px | Table cells, compact cards |
| Medium (md) | 24px | 4px | Default for forms and reviews |
| Large (lg) | 32px | 6px | Hero ratings, primary review input |
| Extra-large (xl) | 48px | 8px | Full-page review submission, marketing |
| Property | Type | Default | Description |
|---|---|---|---|
value | number | — | Current rating value (controlled) |
defaultValue | number | 0 | Initial value (uncontrolled) |
max | number | 5 | Maximum rating value (number of icons) |
precision | 0.25 | 0.5 | 1 | 1 | Minimum selectable increment |
size | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Icon size |
readOnly | boolean | false | Display only, no interaction |
disabled | boolean | false | Prevents interaction with dimmed styling |
onChange | (value: number) => void | — | Callback when user selects a rating |
onHoverChange | (value: number | null) => void | — | Callback during hover preview |
icon | ReactNode | ★ | Custom filled icon |
emptyIcon | ReactNode | ☆ | Custom empty (unfilled) icon |
highlightSelectedOnly | boolean | false | Only fills the selected star (not all up to it) |
label | string | — | Accessible label for the rating group |
showValue | boolean | false | Displays numeric value alongside stars |
labels | string[] | — | Text labels per value (["Terrible", "Poor", "OK", "Good", "Excellent"]) |
Important: The label prop is required for accessibility. Screen readers need to know what is being rated: "Rate this product" vs. "Rate your experience." Without it, users hear "3 out of 5" with no context.
| Token Category | Token Example | Rating Usage |
|---|---|---|
| Color – Filled | --color-yellow-400 / --color-amber-400 | Filled star color |
| Color – Empty | --color-gray-300 | Empty star outline/fill |
| Color – Hover | --color-yellow-300 | Star color during hover preview |
| Color – Disabled | --color-gray-200 | Disabled state star color |
| Color – Text | --color-text-secondary | Numeric value and label text |
| Spacing – Gap | --space-1 (4px) | Gap between star icons |
| Spacing – Label Gap | --space-2 (8px) | Gap between stars and numeric label |
| Icon Size | 16px / 24px / 32px / 48px | Per size variant |
| Transition | --duration-fast (150ms) | Fill/unfill transition on hover |
| Animation | --ease-spring | Star fill scale animation (see Animation Tool) |
| Focus Ring | --color-primary-500, 2px offset | Focus indicator on the rating group |
Star icons should be SVGs, not Unicode characters, for consistent rendering and precise partial-fill control using clipPath or gradient techniques.
| State | Visual Change | Notes |
|---|---|---|
| Empty | All stars unfilled (outlined or gray). | Default before any rating is set. |
| Filled | Stars filled up to the selected value (golden/yellow). | Filled stars use the filled icon; remaining use the empty icon. |
| Hover preview | Stars fill up to the hovered position. Existing selection dims or remains visible. | Only for interactive ratings. Use the Animation Tool to preview smooth fill transitions. |
| Half-filled (read-only) | A star partially filled to represent fractional values. | Use CSS clip-path or SVG gradient for precise fill percentages. |
| Focused | Focus ring around the entire rating group, or individual star highlight. | See keyboard pattern in Accessibility section. |
| Disabled | All stars gray/muted. No hover or click response. | aria-disabled="true" on the group. |
| Read-only | Fractional fill, no hover effects, cursor default. | Visually similar to filled but without interactive affordances. Add aria-label with the exact value: "Rated 4.3 out of 5." |
| Animating | Brief scale-up or bounce on the selected star when clicked. | Use the Animation Tool to configure spring easing. Respect prefers-reduced-motion. |
Ratings present a unique accessibility challenge: they are a custom input widget rendered as a series of icons with no native HTML equivalent. The implementation must be explicit about roles, values, and keyboard interaction.
Recommended ARIA pattern — Radio Group: The most accessible approach treats the rating as a radio group:
role="radiogroup" with aria-label="Rate this product"role="radio" with aria-checked="true|false" and aria-label="1 star" / aria-label="2 stars", etc.Alternative pattern — Slider:
role="slider" with aria-valuemin="0", aria-valuemax="5", aria-valuenow="3", aria-valuetext="3 out of 5 stars", and aria-label="Rating".WCAG 2.1.1 — Keyboard: Using the radio group pattern:
Using the slider pattern:
WCAG 1.4.1 — Use of Color: Star color alone must not be the only means of communicating the rating. The filled vs. unfilled icon shape provides a secondary visual channel (solid fill vs. outline). Ensure this shape difference is perceivable. Verify with the Contrast Checker that both filled and empty stars meet 3:1 non-text contrast against the background (WCAG 1.4.11).
WCAG 1.4.3 — Contrast: The filled star icon (typically yellow/amber) against a white background can be problematic. Yellow (#FACC15) on white fails 3:1. Use a darker amber (#D97706, contrast 3.5:1) or add a dark outline to the stars. Always verify with the Contrast Checker.
WCAG 4.1.2 — Name, Role, Value: Read-only ratings must have an accessible label that includes the numeric value: aria-label="Rated 4.3 out of 5 stars". Don't rely on the visual star fill alone. Screen readers cannot interpret partial SVG fills.
WCAG 2.4.7 — Focus Visible: The focused star or rating group must display a visible focus indicator. A focus ring around the entire group (rather than individual stars) is acceptable if using the slider pattern.
Read-only ratings: Use role="img" with aria-label="4.3 out of 5 stars" for the simplest read-only implementation. No keyboard interaction needed since there's nothing to operate.
Do:
Don't:
Mobile considerations:
Aggregate display best practices:
<!-- Interactive rating -->
<div role="radiogroup" aria-label="Rate this product" class="rating">
<label class="rating__star">
<input type="radio" name="rating" value="1" class="sr-only" />
<svg class="rating__icon" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87L18.18 22 12 18.27 5.82 22 7 14.14 2 9.27l6.91-1.01L12 2z" />
</svg>
<span class="sr-only">1 star</span>
</label>
<label class="rating__star">
<input type="radio" name="rating" value="2" class="sr-only" />
<svg class="rating__icon" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87L18.18 22 12 18.27 5.82 22 7 14.14 2 9.27l6.91-1.01L12 2z" />
</svg>
<span class="sr-only">2 stars</span>
</label>
<label class="rating__star">
<input type="radio" name="rating" value="3" class="sr-only" />
<svg class="rating__icon" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87L18.18 22 12 18.27 5.82 22 7 14.14 2 9.27l6.91-1.01L12 2z" />
</svg>
<span class="sr-only">3 stars</span>
</label>
<label class="rating__star">
<input type="radio" name="rating" value="4" class="sr-only" />
<svg class="rating__icon" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87L18.18 22 12 18.27 5.82 22 7 14.14 2 9.27l6.91-1.01L12 2z" />
</svg>
<span class="sr-only">4 stars</span>
</label>
<label class="rating__star">
<input type="radio" name="rating" value="5" class="sr-only" />
<svg class="rating__icon" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87L18.18 22 12 18.27 5.82 22 7 14.14 2 9.27l6.91-1.01L12 2z" />
</svg>
<span class="sr-only">5 stars</span>
</label>
</div>
<!-- Read-only rating -->
<div role="img" aria-label="Rated 4.3 out of 5 stars" class="rating rating--readonly">
<svg class="rating__icon rating__icon--filled" viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87L18.18 22 12 18.27 5.82 22 7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
<!-- repeat for remaining stars, last star uses clip-path for 30% fill -->
<span class="rating__value">4.3</span>
<span class="rating__count">(2,847)</span>
</div>
<style>
.rating {
display: inline-flex;
align-items: center;
gap: 4px;
}
.rating__star {
cursor: pointer;
display: flex;
}
.rating__icon {
fill: var(--color-gray-300);
transition: fill 150ms, transform 150ms;
}
.rating__star:hover .rating__icon,
.rating__star:has(input:checked) .rating__icon,
.rating__star:has(~ .rating__star input:checked) ~ .rating__star .rating__icon {
/* Note: real implementation uses JS for proper fill-up-to logic */
fill: var(--color-amber-400);
}
.rating__icon--filled {
fill: var(--color-amber-400);
}
.rating__star input:focus-visible + .rating__icon {
outline: 2px solid var(--color-primary-500);
outline-offset: 2px;
border-radius: 2px;
}
.rating__value {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-semibold);
margin-left: var(--space-1);
}
.rating__count {
font-size: var(--font-size-xs);
color: var(--color-text-secondary);
}
/* Star fill animation */
@keyframes star-pop {
0% { transform: scale(1); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
.rating__star input:checked + .rating__icon {
animation: star-pop 300ms var(--ease-spring);
}
@media (prefers-reduced-motion: reduce) {
.rating__star input:checked + .rating__icon {
animation: none;
}
}
</style>interface RatingProps {
value?: number;
defaultValue?: number;
max?: number;
precision?: 0.5 | 1;
size?: 'sm' | 'md' | 'lg' | 'xl';
readOnly?: boolean;
disabled?: boolean;
onChange?: (value: number) => void;
onHoverChange?: (value: number | null) => void;
label: string;
showValue?: boolean;
labels?: string[];
}
function Rating({
value: controlledValue,
defaultValue = 0,
max = 5,
precision = 1,
size = 'md',
readOnly = false,
disabled = false,
onChange,
onHoverChange,
label,
showValue = false,
labels,
}: RatingProps) {
const [internalValue, setInternalValue] = React.useState(defaultValue);
const [hoverValue, setHoverValue] = React.useState<number | null>(null);
const value = controlledValue ?? internalValue;
const displayValue = hoverValue ?? value;
const iconSizes = { sm: 16, md: 24, lg: 32, xl: 48 };
const iconSize = iconSizes[size];
const handleSelect = (newValue: number) => {
if (readOnly || disabled) return;
setInternalValue(newValue);
onChange?.(newValue);
};
const handleHover = (val: number | null) => {
if (readOnly || disabled) return;
setHoverValue(val);
onHoverChange?.(val);
};
// Read-only: use role="img"
if (readOnly) {
return (
<div
className={`rating rating--readonly rating--${size}`}
role="img"
aria-label={`${label}: ${value} out of ${max} stars`}
>
{Array.from({ length: max }, (_, i) => {
const fillPercent = Math.min(1, Math.max(0, value - i));
return (
<StarIcon
key={i}
size={iconSize}
fillPercent={fillPercent}
/>
);
})}
{showValue && <span className="rating__value">{value.toFixed(1)}</span>}
</div>
);
}
// Interactive: use radiogroup
return (
<div
className={`rating rating--${size}`}
role="radiogroup"
aria-label={label}
aria-disabled={disabled || undefined}
onMouseLeave={() => handleHover(null)}
>
{Array.from({ length: max }, (_, i) => {
const starValue = i + 1;
const halfValue = i + 0.5;
const isSelected = starValue === value;
if (precision === 0.5) {
return (
<span key={i} className="rating__star-group" style={{ position: 'relative' }}>
{/* Left half */}
<label
className="rating__half rating__half--left"
onMouseEnter={() => handleHover(halfValue)}
>
<input
type="radio"
name="rating"
value={halfValue}
checked={value === halfValue}
onChange={() => handleSelect(halfValue)}
className="sr-only"
disabled={disabled}
/>
</label>
{/* Right half */}
<label
className="rating__half rating__half--right"
onMouseEnter={() => handleHover(starValue)}
>
<input
type="radio"
name="rating"
value={starValue}
checked={value === starValue}
onChange={() => handleSelect(starValue)}
className="sr-only"
disabled={disabled}
/>
</label>
<StarIcon
size={iconSize}
fillPercent={Math.min(1, Math.max(0, displayValue - i))}
/>
<span className="sr-only">{starValue} star{starValue !== 1 ? 's' : ''}</span>
</span>
);
}
return (
<label
key={i}
className="rating__star"
onMouseEnter={() => handleHover(starValue)}
>
<input
type="radio"
name="rating"
value={starValue}
checked={isSelected}
onChange={() => handleSelect(starValue)}
className="sr-only"
disabled={disabled}
/>
<StarIcon
size={iconSize}
fillPercent={displayValue >= starValue ? 1 : 0}
animated={isSelected}
/>
<span className="sr-only">{starValue} star{starValue !== 1 ? 's' : ''}</span>
</label>
);
})}
{showValue && <span className="rating__value">{displayValue}</span>}
{labels && labels[displayValue - 1] && (
<span className="rating__label">{labels[displayValue - 1]}</span>
)}
</div>
);
}
// Star icon with partial fill support
function StarIcon({ size, fillPercent, animated }: { size: number; fillPercent: number; animated?: boolean }) {
const id = React.useId();
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
className={`rating__icon${animated ? ' rating__icon--pop' : ''}`}
aria-hidden="true"
>
<defs>
<linearGradient id={`fill-${id}`}>
<stop offset={`${fillPercent * 100}%`} stopColor="var(--color-amber-400)" />
<stop offset={`${fillPercent * 100}%`} stopColor="var(--color-gray-300)" />
</linearGradient>
</defs>
<path
d="M12 2l3.09 6.26L22 9.27l-5 4.87L18.18 22 12 18.27 5.82 22 7 14.14 2 9.27l6.91-1.01L12 2z"
fill={`url(#fill-${id})`}
/>
</svg>
);
}Material Design 3 provides Rating through MUI with value, precision (supports any decimal like 0.1), size="small|medium|large", readOnly, disabled, max, icon (filled), emptyIcon, highlightSelectedOnly, and getLabelText (custom accessible label per value). MUI's implementation uses the radio group pattern with roving tabindex. The hover preview shows a tooltip with the value label ("3 Stars"). Partial fills use SVG gradient stops. MUI also supports custom icons — replacing stars with hearts, thumbs, or any SVG. The IconContainerComponent prop allows complete control over each icon's wrapper for advanced hover effects.
Ant Design provides Rate with count (number of stars), allowHalf, allowClear (clicking the same value again resets to 0), character (custom icon — can be a React node or function receiving the index), tooltips (array of tooltip strings per value), and disabled. Ant's implementation focuses on simplicity — fewer configuration options but solid defaults. The character prop accepting a function is powerful: character={({ index }) => index + 1} renders numbers instead of stars. Ant uses CSS transitions for fill changes and a brief scale animation on selection.
Chakra UI does not provide a dedicated Rating component as of v2. Developers typically compose one from HStack and IconButton with custom state management. This is a notable gap compared to MUI and Ant.
Radix UI does not provide a Rating primitive. The radio group pattern can be used as a foundation, but fractional display and hover preview require custom implementation.
Headless UI does not include a Rating component.
react-rating and @smastrom/react-rating are popular community libraries that provide accessible, headless rating components with half-star support, custom shapes (SVG paths), CSS-based partial fill, and the full radio group ARIA pattern. These are strong starting points for custom design systems.
Use the Animation Tool to configure the star selection animation — a spring-based scale (1 → 1.2 → 1) with 200–300ms duration feels satisfying without being distracting. Always respect prefers-reduced-motion.