Search

Ctrl + K

Rating

This document outlines the steps to create Rating component styled withTailwind CSS and using some npm dependency libraries.

Prerequisites

  • 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.
Step 1: Create variant styles
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
Step 2: Create Rating component
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