This document outlines the steps to create Rating
component styled withTailwind CSS and using some npm dependency libraries.
Node.js
and npm
installed on your machine.Tailwind CSS
installed in your project.CVA(class-variance-authority)
is a utility for managing CSS class names based on various conditions.clsx
is a tiny utility for constructing className strings conditionally.1// rating.helpers.ts
2import { cva } from "class-variance-authority";
3
4export const STAR_ICON_URI =
5 "data:image/svg+xml;charset=utf-8;base64,PHN2ZyB3aWR0aD0nMTkyJyBoZWlnaHQ9JzE4MCcgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJz48cGF0aCBmaWxsPSdibGFjaycgZD0nbTk2IDE1My4wNDQtNTguNzc5IDI2LjI0MyA3LjAyLTYzLjUxM0wuODk0IDY4LjQ4MWw2My4xMTctMTMuMDFMOTYgMGwzMS45ODkgNTUuNDcyIDYzLjExNyAxMy4wMS00My4zNDcgNDcuMjkyIDcuMDIgNjMuNTEzeicgZmlsbC1ydWxlPSdldmVub2RkJy8+PC9zdmc+";
6
7export const ratingVariants = cva(
8 "appearance-none transition-all duration-150 disabled:active:translate-y-0 active:-translate-y-0.5 bg-neutral-300 dark:bg-neutral-500 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",
9 {
10 variants: {
11 color: {
12 primary: "data-[checked=true]:!bg-blue-500",
13 secondary: "data-[checked=true]:!bg-gray-500",
14 success: "data-[checked=true]:!bg-green-500",
15 danger: "data-[checked=true]:!bg-red-500",
16 warning: "data-[checked=true]:!bg-yellow-500",
17 info: "data-[checked=true]:!bg-cyan-500",
18 light: "data-[checked=true]:!bg-neutral-400",
19 dark: "data-[checked=true]:!bg-black",
20 },
21 size: {
22 small:
23 "data-[half=true]:w-3 data-[half=false]:w-6 h-6 data-[left=true]:ml-2",
24 medium:
25 "data-[half=true]:w-5 data-[half=false]:w-10 h-10 data-[left=true]:ml-2",
26 large:
27 "data-[half=true]:w-8 data-[half=false]:w-16 h-16 data-[left=true]:ml-4",
28 },
29 },
30 },
31);
32
1// rating.component.tsx
2import clsx from "clsx";
3import type { VariantProps } from "class-variance-authority";
4import { Fragment, useId } from "react";
5import { ratingVariants, STAR_ICON_URI } from "./rating.helpers";
6
7export interface RatingProps extends VariantProps<typeof ratingVariants> {
8 name?: string;
9 value: number;
10 total?: number;
11 hasHalfValue?: boolean;
12 disabled?: boolean;
13 className?: string;
14 onChange: (value: number) => void;
15}
16
17const Rating: React.FC<RatingProps> = ({
18 name,
19 value,
20 total = 5,
21 hasHalfValue = false,
22 disabled,
23 color = "primary",
24 size = "medium",
25 className,
26 onChange,
27}) => {
28 const id = useId();
29
30 return (
31 <div className={clsx("flex items-center", className)}>
32 {Array.from({ length: Math.floor(total) }, (_, i) => i + 1).map(
33 (star) => (
34 <Fragment key={star}>
35 {Array.from({ length: 2 }).map((_, index) => {
36 if (!hasHalfValue && index === 1) return null;
37
38 const radioValue =
39 hasHalfValue && index === 0 ? star - 0.5 : star;
40
41 const maskImage = `url(${STAR_ICON_URI})`;
42 const maskPosition = hasHalfValue
43 ? index
44 ? "right"
45 : "left"
46 : "center";
47 const maskSize = hasHalfValue ? "200%" : "contain";
48 const maskRepeat = "no-repeat";
49
50 return (
51 <input
52 key={index}
53 type="radio"
54 name={name || id}
55 value={radioValue}
56 data-checked={radioValue <= value}
57 data-left={index === 0 && star !== 1}
58 data-half={hasHalfValue}
59 disabled={disabled}
60 className={ratingVariants({ color, size })}
61 style={{
62 maskImage,
63 WebkitMaskImage: maskImage,
64 maskPosition,
65 WebkitMaskPosition: maskPosition,
66 maskSize,
67 WebkitMaskSize: maskSize,
68 maskRepeat,
69 WebkitMaskRepeat: maskRepeat,
70 }}
71 onChange={() => onChange(radioValue)}
72 />
73 );
74 })}
75 </Fragment>
76 ),
77 )}
78 </div>
79 );
80};
81
82export default Rating;
83