VibeUI
ComponentsDocs
VibeUI

Components with taste, that you own. Copy the source, retint the tokens, keep every line.

Library

  • All components
  • Signature pieces
  • Text effects
  • Footers

Docs

  • Introduction
  • Installation
  • Theming
  • CLI

Project

  • GitHub
  • Agents and MCP
  • Issues
  • MIT license
© 2026 VibeUI · MIT licensed
  1. Components
  2. Forms
  3. Rating

Rating

Star rating with hover preview. Use readOnly to display a score.

FormsClient component
Read only

Install

bash
npx @agents-npm-packages/vibeui add rating --dir ./src/components/ui

Import

tsx
import { Rating } from "@/components/ui/rating"

API

Generated from the component's TypeScript types, so it always matches the source. * marks a required prop.

<Rating>

PropTypeDefaultDescription
valuenumber—Controlled value.
defaultValuenumber0Initial value when uncontrolled.
onChange((value: number) => void)—Called when the value changes.
maxnumber5Upper bound.
sizenumber20Size preset.
classNamestring—Extra classes, merged with the component's own.
readOnlyboolean—Displays the rating without allowing changes.

Source

src/components/ui/rating.tsx
"use client";

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

/** Star rating with hover preview. Use readOnly to display a score. */
export function Rating({
  value,
  defaultValue = 0,
  onChange,
  max = 5,
  size = 20,
  className,
  readOnly,
}: {
  value?: number;
  defaultValue?: number;
  onChange?: (value: number) => void;
  max?: number;
  size?: number;
  className?: string;
  readOnly?: boolean;
}) {
  const [uncontrolled, setUncontrolled] = React.useState(defaultValue);
  const [hover, setHover] = React.useState<number | null>(null);
  const current = value ?? uncontrolled;
  const display = hover ?? current;

  return (
    <div
      role={readOnly ? "img" : "radiogroup"}
      aria-label={`Rating: ${current} of ${max}`}
      className={cn("inline-flex items-center", className)}
      onMouseLeave={() => setHover(null)}
    >
      {Array.from({ length: max }, (_, i) => {
        const n = i + 1;
        const filled = n <= display;
        return (
          <button
            key={n}
            type="button"
            role={readOnly ? undefined : "radio"}
            aria-checked={readOnly ? undefined : n === current}
            disabled={readOnly}
            aria-label={`${n} star${n === 1 ? "" : "s"}`}
            onMouseEnter={() => !readOnly && setHover(n)}
            onClick={() => {
              if (readOnly) return;
              setUncontrolled(n);
              onChange?.(n);
            }}
            className="rounded-sm p-0.5 transition-transform duration-150 enabled:hover:scale-110 enabled:active:scale-95 disabled:cursor-default"
          >
            <Star
              size={size}
              weight={filled ? "fill" : "regular"}
              className={cn(
                "transition-colors duration-150",
                filled ? "text-accent" : "text-line-strong",
              )}
            />
          </button>
        );
      })}
    </div>
  );
}

Exports Rating. Peer dependencies: clsx, tailwind-merge, @phosphor-icons/react.

PreviousRange SliderNextSearch Field