Components
Rating group

Rating Group

Allows users to rate items using a set of icons.

Usage

Star IconStar IconStar IconStar IconStar Icon
import { RatingGroup, type RatingGroupProps } from '~/components/ui/rating-group'

export const Demo = (props: RatingGroupProps) => {
  return (
    <RatingGroup defaultValue={3} {...props}>
      Label
    </RatingGroup>
  )
}

Examples

Different color

Use the colorPalette prop to change the color of the rating group.

Star IconStar IconStar IconStar IconStar Icon
<RatingGroup colorPalette="red" value={3}>
  Label
</RatingGroup>

Rating count

Use the count prop to render a specific number of stars.

Star IconStar IconStar IconStar IconStar IconStar IconStar IconStar IconStar IconStar Icon
<RatingGroup count={10}>Label</RatingGroup>

Half star rating

Use the allowHalf prop to enable half star ratings.

Star IconStar IconStar IconStar IconStar Icon
<RatingGroup value={3.5} allowHalf>
  Label
</RatingGroup>

Installation

1

Add Component

Insert code snippet into your project. Update import paths as needed.

import {
  RatingGroup as ArkRatingGroup,
  type RatingGroupRootProps,
} from '@ark-ui/react/rating-group'
import { forwardRef, type ReactNode } from 'react'
import { css, cx } from 'styled-system/css'
import { splitCssProps } from 'styled-system/jsx'
import { ratingGroup, type RatingGroupVariantProps } from 'styled-system/recipes'
import type { Assign, JsxStyleProps } from 'styled-system/types'

export interface RatingGroupProps
  extends Assign<JsxStyleProps, RatingGroupRootProps>,
    RatingGroupVariantProps {
  children?: ReactNode
}

export const RatingGroup = forwardRef<HTMLDivElement, RatingGroupProps>((props, ref) => {
  const [variantProps, ratingGroupProps] = ratingGroup.splitVariantProps(props)
  const [cssProps, localProps] = splitCssProps(ratingGroupProps)
  const { children, className, ...rootProps } = localProps
  const styles = ratingGroup(variantProps)

  return (
    <ArkRatingGroup.Root
      ref={ref}
      className={cx(styles.root, css(cssProps), className)}
      {...rootProps}
    >
      {children && <ArkRatingGroup.Label className={styles.label}>{children}</ArkRatingGroup.Label>}
      <ArkRatingGroup.Control className={styles.control}>
        {({ items }) =>
          items.map((index) => (
            <ArkRatingGroup.Item className={styles.item} key={index} index={index}>
              {({ isHalf }) => <StarIcon isHalf={isHalf} />}
            </ArkRatingGroup.Item>
          ))
        }
      </ArkRatingGroup.Control>
    </ArkRatingGroup.Root>
  )
})

RatingGroup.displayName = 'RatingGroup'

type IconProps = {
  isHalf: boolean
}

const StarIcon = (props: IconProps) => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width="24"
    height="24"
    viewBox="0 0 24 24"
    fill="inherit"
    stroke="inherit"
    strokeWidth="2"
    strokeLinecap="round"
    strokeLinejoin="round"
  >
    <title>Star Icon</title>
    <defs>
      <linearGradient id="half">
        <stop offset="50%" stopColor="var(--colors-color-palette-default)" />
        <stop offset="50%" stopColor="var(--colors-bg-emphasized)" />
      </linearGradient>
    </defs>
    <polygon
      fill={props.isHalf ? 'url(#half)' : 'inherit'}
      points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"
    />
  </svg>
)
2

Add Recipe

This step is necessary only if you do not use any of the Park UI plugins.

import { ratingGroupAnatomy } from '@ark-ui/anatomy'
import { defineSlotRecipe } from '@pandacss/dev'

export const ratingGroup = defineSlotRecipe({
  className: 'ratingGroup',
  slots: ratingGroupAnatomy.keys(),
  base: {
    root: {
      colorPalette: 'accent',
      display: 'flex',
      flexDirection: 'column',
      gap: '1.5',
    },
    label: {
      color: 'fg.default',
      fontWeight: 'medium',
    },
    control: {
      display: 'flex',
    },
    item: {
      cursor: 'pointer',
      transitionDuration: 'normal',
      transitionProperty: 'color, fill',
      transitionTimingFunction: 'default',
      fill: 'bg.emphasized',
      _highlighted: {
        fill: 'colorPalette.default',
      },
      _focusVisible: {
        outline: 'none',
      },
    },
  },
  defaultVariants: {
    size: 'md',
  },
  variants: {
    size: {
      sm: {
        control: {
          gap: '0',
        },
        item: {
          '& svg': {
            width: '4',
            height: '4',
          },
        },
        label: {
          textStyle: 'sm',
        },
      },
      md: {
        control: {
          gap: '0.5',
        },
        item: {
          '& svg': {
            width: '5',
            height: '5',
          },
        },
        label: {
          textStyle: 'sm',
        },
      },
      lg: {
        control: {
          gap: '0.5',
        },
        item: {
          '& svg': {
            width: '6',
            height: '6',
          },
        },
        label: {
          textStyle: 'md',
        },
      },
    },
  },
})