Overview
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:
- To collect user feedback on a 1–5 (or 1–10) scale
- To display average ratings in product cards, search results, and reviews
- For satisfaction surveys (emoji scales, star ratings)
- In dashboards showing aggregated quality metrics
When NOT to use a Rating:
- For binary feedback (like/dislike) — use a toggle button or Switch
- For continuous range values — use a Slider
- For selecting options from a list — use Radio Buttons
- For detailed qualitative feedback — use a textarea or Text Input
Preview star fill animations with the Animation Tool, and verify that star colors meet contrast requirements using the Contrast Checker.
Variants
Rating Variants
| 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 Variants
| 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 Variants
| 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 |
Properties
Rating Properties
| 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 Mappings
Design Token Mappings
| 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.
States
Rating States
| 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. |
Accessibility
Accessibility
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:
- Container:
role="radiogroup"witharia-label="Rate this product" - Each star:
role="radio"witharia-checked="true|false"andaria-label="1 star"/aria-label="2 stars", etc. - This maps perfectly to the mental model: select one value from a set of mutually exclusive options.
Alternative pattern — Slider:
- Container:
role="slider"witharia-valuemin="0",aria-valuemax="5",aria-valuenow="3",aria-valuetext="3 out of 5 stars", andaria-label="Rating". - This is simpler to implement but less intuitive — sliders imply continuous values, while ratings are discrete.
WCAG 2.1.1 — Keyboard: Using the radio group pattern:
- Tab: Focus enters/exits the rating group (single tab stop)
- ← / ↓: Decrease rating by one step
- → / ↑: Increase rating by one step
- Home: Set to minimum (1)
- End: Set to maximum (5)
- Space: Select the focused value (if using roving focus)
Using the slider pattern:
- ← / ↓: Decrease by precision step
- → / ↑: Increase by precision step
- Home / End: Min / max value
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.
Usage Guidelines
Usage Guidelines
Do:
- Always include a text label describing what is being rated ("Rate this product", "How was your experience?")
- Show the numeric value alongside stars for precision (★★★★☆ 4.0)
- Use half-star precision for input when users expect granularity (reviews)
- Provide text labels per value when possible ("Poor", "Fair", "Good", "Very Good", "Excellent")
- Animate star fills on selection for satisfying feedback — configure with the Animation Tool
- Use consistent star coloring (golden/amber) across your application
Don't:
- Don't use more than 10 icons in a rating scale — 5 is the universal standard, 10 is the practical maximum
- Don't use yellow stars on white backgrounds without checking contrast — the standard yellow fails WCAG
- Don't auto-submit a form when a rating is selected — users may want to change their selection
- Don't show decimal places for user input ("4.0" not "4.00") — decimals are for display/aggregates
- Don't hide the interactive rating behind a "Write a review" expansion — make it immediately accessible
- Don't use ratings for binary choices — a like/dislike button is simpler and clearer
Mobile considerations:
- Increase touch target size to at least 44×44px per star (WCAG 2.5.5 Target Size Enhanced, 2.5.8 Target Size Minimum)
- Remove hover preview on touch devices (no hover exists) — provide tap-to-set with optional confirmation
- Consider a horizontal swipe gesture for rating selection on mobile
- Show the selected value prominently since touch precision is lower than pointer precision
Aggregate display best practices:
- Show the average rating with one decimal place (4.3, not 4.27)
- Include the total number of reviews: "4.3 ★ (2,847 reviews)"
- Consider showing the rating distribution (histogram of 1-star through 5-star counts)
Code Snippets
<!-- 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>
);
}Design Systems
Design System Implementations
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.