Stars
Fractional star rating with count, filled by a clipped overlay.
ui
$
npx shadcn@latest add @storekit/starsstars
Live and interactive. Switch the brand in the frame header to see the same source under another token file.
Installation
$
npx shadcn@latest add @storekit/stars$
pnpm dlx shadcn@latest add @storekit/stars$
yarn dlx shadcn@latest add @storekit/stars$
bunx --bun shadcn@latest add @storekit/starsSource
components/storekit/ui/stars.tsx
47 lines
import { useId } from "react"
import { cn } from "@/components/storekit/lib/cn"
/* Fractional star rating. Each star clips its own fill to the fraction it
earns, so 4.6 reads as four full stars and a 60% fifth. */
const PATH = "M12 2.5l2.9 6.1 6.7.8-4.9 4.6 1.3 6.6L12 17.3l-6 3.3 1.3-6.6L2.4 9.4l6.7-.8L12 2.5z"
export function Stars({
value,
count,
size = 14,
className,
showValue = false,
}: {
value: number
count?: number
size?: number
className?: string
showValue?: boolean
}) {
const id = useId()
return (
<span className={cn("inline-flex items-center gap-2", className)} aria-label={`${value.toFixed(1)} out of 5`}>
<span className="flex gap-0.5" aria-hidden>
{Array.from({ length: 5 }).map((_, i) => {
const frac = Math.max(0, Math.min(1, value - i))
const clip = `${id}-${i}`
return (
<svg key={i} width={size} height={size} viewBox="0 0 24 24">
<defs>
<clipPath id={clip}>
<rect x="0" y="0" width={24 * frac} height="24" />
</clipPath>
</defs>
<path d={PATH} className="fill-sk-ink/15" />
{frac > 0 ? <path d={PATH} className="fill-sk-accent" clipPath={`url(#${clip})`} /> : null}
</svg>
)
})}
</span>
{showValue ? <span className="text-sm font-medium tabular-nums text-sk-ink">{value.toFixed(1)}</span> : null}
{typeof count === "number" ? <span className="text-sm tabular-nums text-sk-muted">({count.toLocaleString()})</span> : null}
</span>
)
}
Usage
Import from "@/components/storekit/ui/stars" and pass your product data. The demo above exercises the full prop surface with sample values.