Skip to content

Rating

Allows users to rate or view ratings using a visual scale (typically stars).

  • Star Rating
  • Review

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

VariantPurposeVisual Treatment
InteractiveUser input — click/tap to set a rating.Filled stars up to the selected value, empty stars beyond. Hover preview.
Read-onlyDisplay an existing rating.Fractional fill (e.g., 4.3 stars = 4 full + 1 at 30%). No interaction.
CompactSpace-constrained contexts (table cells, cards).Single star icon + numeric label (★ 4.3).
EmojiSentiment scale (1=😡, 5=😍).Replaces stars with emoji or face icons. Common for CSAT surveys.
HeartAffection-based ratings (favorites, love).Uses ♥ instead of ★. Common on social/content platforms.
Custom iconDomain-specific scales.Any icon — thumbs up, flame, diamond, etc.

Precision Variants

PrecisionDescriptionUse 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

SizeIcon SizeGapUse Case
Small (sm)16px2pxTable cells, compact cards
Medium (md)24px4pxDefault for forms and reviews
Large (lg)32px6pxHero ratings, primary review input
Extra-large (xl)48px8pxFull-page review submission, marketing

Properties

Rating Properties

PropertyTypeDefaultDescription
valuenumber—Current rating value (controlled)
defaultValuenumber0Initial value (uncontrolled)
maxnumber5Maximum rating value (number of icons)
precision0.25 | 0.5 | 11Minimum selectable increment
size'sm' | 'md' | 'lg' | 'xl''md'Icon size
readOnlybooleanfalseDisplay only, no interaction
disabledbooleanfalsePrevents interaction with dimmed styling
onChange(value: number) => void—Callback when user selects a rating
onHoverChange(value: number | null) => void—Callback during hover preview
iconReactNode★Custom filled icon
emptyIconReactNode☆Custom empty (unfilled) icon
highlightSelectedOnlybooleanfalseOnly fills the selected star (not all up to it)
labelstring—Accessible label for the rating group
showValuebooleanfalseDisplays numeric value alongside stars
labelsstring[]—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 CategoryToken ExampleRating Usage
Color – Filled--color-yellow-400 / --color-amber-400Filled star color
Color – Empty--color-gray-300Empty star outline/fill
Color – Hover--color-yellow-300Star color during hover preview
Color – Disabled--color-gray-200Disabled state star color
Color – Text--color-text-secondaryNumeric 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 Size16px / 24px / 32px / 48pxPer size variant
Transition--duration-fast (150ms)Fill/unfill transition on hover
Animation--ease-springStar fill scale animation (see Animation Tool)
Focus Ring--color-primary-500, 2px offsetFocus 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

StateVisual ChangeNotes
EmptyAll stars unfilled (outlined or gray).Default before any rating is set.
FilledStars filled up to the selected value (golden/yellow).Filled stars use the filled icon; remaining use the empty icon.
Hover previewStars 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.
FocusedFocus ring around the entire rating group, or individual star highlight.See keyboard pattern in Accessibility section.
DisabledAll stars gray/muted. No hover or click response.aria-disabled="true" on the group.
Read-onlyFractional 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."
AnimatingBrief 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" with aria-label="Rate this product"
  • Each star: role="radio" with aria-checked="true|false" and aria-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" with aria-valuemin="0", aria-valuemax="5", aria-valuenow="3", aria-valuetext="3 out of 5 stars", and aria-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

html
<!-- 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>
tsx
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.

Data DisplayStar RatingReview