Rating
React formStar (or custom icon) rating control — pick a value from 1 to N. Read-only mode for displaying review averages, with half-step support.
Also available for Vue ->Installation
$ pnpm dlx shadcn@latest add https://react.uipkge.dev/r/react/rating.json$ npx shadcn@latest add https://react.uipkge.dev/r/react/rating.json$ yarn dlx shadcn@latest add https://react.uipkge.dev/r/react/rating.json$ bunx shadcn@latest add https://react.uipkge.dev/r/react/rating.json
Or with the named registry:
npx shadcn@latest add @uipkge-react/rating
Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
value Currently selected value | number | — | optional |
defaultValue Default value when uncontrolled | number | — | optional |
onValueChange Emitted when the value changes | (value: number) => void | — | optional |
max Maximum rating value | number | — | optional |
readonly If true, prevents user interaction | boolean | — | optional |
disabled If true, disables the rating | boolean | — | optional |
density Density of the component | 'compact' | 'default' | 'comfortable' | — | optional |
color Color of the selected stars | string | — | optional |
clearable If true, clicking the same value clears the rating | boolean | — | optional |
hover If true, stars grow on hover | boolean | — | optional |
itemAriaLabel ARIA label for each rating item | string | — | optional |
size Size of the stars | 'x-small' | 'small' | 'medium' | 'large' | 'x-large' | — | optional |
className Custom class for the component | string | — | optional |
showValue Show rating count | boolean | — | optional |
variant Card variant styling | 'outlined' | 'filled' | 'soft' | — | optional |
halfIncrements If true, creates a half star at 0.5 | boolean | — | optional |
Files (2)
-
components/ui/rating/Rating.tsx 6 kB
'use client' import * as React from 'react' import { cn } from '@/lib/utils' export interface RatingProps { /** Currently selected value */ value?: number /** Default value when uncontrolled */ defaultValue?: number /** Emitted when the value changes */ onValueChange?: (value: number) => void /** Maximum rating value */ max?: number /** If true, prevents user interaction */ readonly?: boolean /** If true, disables the rating */ disabled?: boolean /** Density of the component */ density?: 'compact' | 'default' | 'comfortable' /** Color of the selected stars */ color?: string /** If true, clicking the same value clears the rating */ clearable?: boolean /** If true, stars grow on hover */ hover?: boolean /** ARIA label for each rating item */ itemAriaLabel?: string /** Size of the stars */ size?: 'x-small' | 'small' | 'medium' | 'large' | 'x-large' /** Custom class for the component */ className?: string /** Show rating count */ showValue?: boolean /** Card variant styling */ variant?: 'outlined' | 'filled' | 'soft' /** If true, creates a half star at 0.5 */ halfIncrements?: boolean } // Star path shared by all three icon states. const STAR_PATH = 'M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z' // Ported 1:1 from the Vue scoped stylesheet. React has no scoped styles, so the // per-size star dimensions and per-variant container chrome live inline here. const variantClasses: Record<NonNullable<RatingProps['variant']>, string> = { outlined: '', filled: 'bg-muted p-1 rounded-lg', soft: 'bg-accent p-1 rounded-lg', } const sizeIcon: Record<NonNullable<RatingProps['size']>, React.CSSProperties> = { 'x-small': { width: '0.875rem', height: '0.875rem' }, small: { width: '1.125rem', height: '1.125rem' }, medium: { width: '1.375rem', height: '1.375rem' }, large: { width: '1.625rem', height: '1.625rem' }, 'x-large': { width: '2rem', height: '2rem' }, } const densityPad: Record<NonNullable<RatingProps['density']>, string> = { compact: '0', default: '0.0625rem', comfortable: '0.125rem', } const Rating = React.forwardRef<HTMLDivElement, RatingProps>( ( { value, defaultValue = 0, onValueChange, max = 5, readonly = false, disabled = false, density = 'default', color = 'var(--warning)', clearable = false, hover = false, itemAriaLabel = 'rating', size = 'medium', className, showValue = false, variant = 'outlined', halfIncrements = false, }, ref, ) => { const isControlled = value !== undefined const [internal, setInternal] = React.useState(defaultValue) const current = isControlled ? value! : internal function emit(next: number) { if (!isControlled) setInternal(next) onValueChange?.(next) } function handleClick(n: number) { if (disabled || readonly) return if (clearable && n === current) emit(0) else emit(n) } function handleKeydown(e: React.KeyboardEvent, n: number) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() handleClick(n) } else if (e.key === 'ArrowRight' && n < max) { emit(n + 1) } else if (e.key === 'ArrowLeft' && n > 1) { emit(n - 1) } } const iconStyle = sizeIcon[size] return ( <div ref={ref} data-uipkge="" data-slot="rating" className={cn( 'inline-flex items-center gap-0.5', variantClasses[variant], disabled && 'cursor-not-allowed opacity-50', readonly && 'cursor-default', showValue && 'flex items-center gap-1', className, )} role="radiogroup" aria-valuenow={current} aria-valuemin={0} aria-valuemax={max} aria-label={`Rating: ${current} of ${max}`} > {Array.from({ length: max }, (_, i) => i + 1).map((n) => ( <button key={n} type="button" disabled={disabled || readonly} aria-label={`${itemAriaLabel} ${n} of ${max}`} tabIndex={readonly || disabled ? -1 : 0} onClick={() => handleClick(n)} onKeyDown={(e) => handleKeydown(e, n)} style={{ padding: densityPad[density], outlineColor: color }} className={cn( 'inline-flex items-center justify-center border-none bg-none leading-none', 'focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none', clearable && 'cursor-pointer', hover && 'transition-transform duration-150 ease-in-out hover:scale-115', )} > {current >= n ? ( // Full star <svg viewBox="0 0 24 24" fill="currentColor" style={{ ...iconStyle, color }}> <path d={STAR_PATH} /> </svg> ) : current >= n - 0.5 && halfIncrements ? ( // Half star <svg viewBox="0 0 24 24" style={{ ...iconStyle, color }}> <defs> <linearGradient id={`half-${n}`}> <stop offset="50%" stopColor="currentColor" /> <stop offset="50%" stopColor="transparent" /> </linearGradient> </defs> <path d={STAR_PATH} fill={`url(#half-${n})`} stroke="currentColor" strokeWidth="1" /> </svg> ) : ( // Empty star <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" style={{ ...iconStyle, color: 'var(--muted-foreground)' }} > <path d={STAR_PATH} /> </svg> )} </button> ))} {showValue && ( <span className="ml-2 font-semibold text-foreground">{current}</span> )} </div> ) }, ) Rating.displayName = 'Rating' export { Rating } -
components/ui/rating/index.ts 0.1 kB
export { Rating, type RatingProps } from './Rating'
Raw manifest: https://react.uipkge.dev/r/react/rating.json