Installation
npx @park-ui/cli add rating-groupAdd Component
Copy the code snippet below into you components folder.
'use client'
import {
RatingGroup,
useRatingGroupContext,
useRatingGroupItemContext,
} from '@ark-ui/react/rating-group'
import { StarIcon } from 'lucide-react'
import {
type ComponentProps,
cloneElement,
forwardRef,
isValidElement,
type ReactElement,
} from 'react'
import { createStyleContext, type HTMLStyledProps } from 'styled-system/jsx'
import { ratingGroup } from 'styled-system/recipes'
const { withProvider, withContext } = createStyleContext(ratingGroup)
export type RootProps = ComponentProps<typeof Root>
export const Root = withProvider(RatingGroup.Root, 'root')
export const RootProvider = withProvider(RatingGroup.RootProvider, 'root')
export const Item = withContext(RatingGroup.Item, 'item')
export const Label = withContext(RatingGroup.Label, 'label')
export const HiddenInput = RatingGroup.HiddenInput
export {
RatingGroupContext as Context,
RatingGroupItemContext as ItemContext,
} from '@ark-ui/react/rating-group'
interface ItemIndicatorProps extends HTMLStyledProps<'span'> {
icon?: ReactElement | undefined
}
const StyledItemIndicator = withContext('span', 'itemIndicator')
const cloneIcon = (icon: ReactElement, type: string) => {
if (!isValidElement(icon)) return null
const props = { [`data-${type}`]: '', 'aria-hidden': true, fill: 'currentColor' }
return cloneElement(icon, props)
}
export const ItemIndicator = forwardRef<HTMLSpanElement, ItemIndicatorProps>(
function ItemIndicator(props, ref) {
const { icon = <StarIcon />, ...rest } = props
const item = useRatingGroupItemContext()
return (
<StyledItemIndicator
ref={ref}
{...rest}
data-highlighted={item.highlighted ? '' : undefined}
data-checked={item.checked ? '' : undefined}
data-half={item.half ? '' : undefined}
>
{cloneIcon(icon, 'bg')}
{cloneIcon(icon, 'fg')}
</StyledItemIndicator>
)
},
)
interface ItemsProps extends Omit<ComponentProps<typeof Item>, 'index'> {
icon?: ReactElement | undefined
}
export const Items = (props: ItemsProps) => {
const { icon, ...rest } = props
const ratingGroup = useRatingGroupContext()
return ratingGroup.items.map((item) => (
<Item key={item} index={item} {...rest}>
<ItemIndicator icon={icon} />
</Item>
))
}
export const Control = withContext(RatingGroup.Control, 'control', {
defaultProps: { children: <Items /> },
})
Integrate Recipe
Integrate this recipe in to your Panda config.
import { ratingGroupAnatomy } from '@ark-ui/react/rating-group/'
import { defineSlotRecipe } from '@pandacss/dev'
export const ratingGroup = defineSlotRecipe({
className: 'rating-group',
slots: ratingGroupAnatomy.extendWith('itemIndicator').keys(),
base: {
root: {
alignItems: 'center',
display: 'inline-flex',
verticalAlign: 'top',
},
control: {
alignItems: 'center',
display: 'inline-flex',
gap: '0.5',
},
item: {
alignItems: 'center',
display: 'inline-flex',
justifyContent: 'center',
userSelect: 'none',
},
label: {
fontWeight: 'medium',
userSelect: 'none',
},
itemIndicator: {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
'--clip-path': { base: 'inset(0 50% 0 0)', _rtl: 'inset(0 0 0 50%)' },
_icon: {
stroke: 'currentColor',
display: 'inline-block',
flexShrink: 0,
position: 'absolute',
width: 'inherit',
height: 'inherit',
left: 0,
top: 0,
},
'& [data-bg]': {
color: 'gray.subtle.bg',
},
'& [data-fg]': {
color: 'transparent',
},
'&[data-highlighted]:not([data-half])': {
'& [data-fg]': {
color: 'colorPalette.solid.bg',
},
},
'&[data-half]': {
'& [data-fg]': {
color: 'colorPalette.solid.bg',
clipPath: 'var(--clip-path)',
},
},
},
},
variants: {
size: {
xs: { root: { gap: '2' }, itemIndicator: { width: '4', height: '4' } },
sm: { root: { gap: '2' }, itemIndicator: { width: '4.5', height: '4.5' } },
md: { root: { gap: '3' }, itemIndicator: { width: '5', height: '5' } },
lg: { root: { gap: '3' }, itemIndicator: { width: '5.5', height: '5.5' } },
xl: { root: { gap: '3' }, itemIndicator: { width: '6', height: '6' } },
},
},
defaultVariants: {
size: 'md',
},
})
Usage
import { RatingGroup } from '@/components/ui'
<RatingGroup.Root>
<RatingGroup.Label />
<RatingGroup.HiddenInput />
<RatingGroup.Control>
<RatingGroup.Item>
<RatingGroup.ItemIndicator />
</RatingGroup.Item>
</RatingGroup.Control>
</RatingGroup.Root>
Shortcuts
The RatingGroup component also provides a set of shortcuts for common use cases.
RatingControl
This component renders the number of rating items specified in the count prop.
This works:
<RatingGroup.Control>
{Array.from({ length: 5 }).map((_, index) => (
<RatingGroup.Item key={index} index={index + 1}>
<RatingGroup.ItemIndicator />
</RatingGroup.Item>
))}
</RatingGroup.Control>
This might be more concise, if you don't need to customize the rating icons:
<RatingGroup.Control />
Examples
Sizes
Use the size prop to change the size of the rating component.
Controlled
Use the value and onValueChange prop to control the rating value.
ReadOnly
Use the readOnly prop to make the rating component read-only.
Custom Icon
Use the icon prop to pass a custom icon to the rating component. This will override the default star icon.
Label
Render the RatingGroup.Label component to provide a human-readable label for the rating component.
Half Star
Use the allowHalf prop to allow half-star ratings.
Colors
Use the colorPalette prop to change the color of the rating.
Closed Component
Here's how to setup the Rating for a closed component composition.
import { forwardRef, type ReactElement, type ReactNode } from 'react'
import { RatingGroup as StyledRatingGroup } from '@/components/ui'
export interface RatingProps extends StyledRatingGroup.RootProps {
icon?: ReactElement
count?: number
label?: ReactNode
}
export const RatingGroup = forwardRef<HTMLDivElement, RatingProps>(function Rating(props, ref) {
const { icon, count = 5, label, ...rest } = props
return (
<StyledRatingGroup.Root ref={ref} count={count} {...rest}>
{label && <StyledRatingGroup.Label>{label}</StyledRatingGroup.Label>}
<StyledRatingGroup.HiddenInput />
<StyledRatingGroup.Control>
<StyledRatingGroup.Items icon={icon} />
</StyledRatingGroup.Control>
</StyledRatingGroup.Root>
)
})
So that you can use it like this:
<RatingGroup defaultValue={3} size="sm" />
Props
Root
| Prop | Default | Type |
|---|---|---|
size | 'md' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' |
count | 5 | numberThe total number of ratings. |
allowHalf | booleanWhether to allow half stars. | |
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. | |
autoFocus | booleanWhether to autofocus the rating. | |
defaultValue | numberThe initial value of the rating when rendered. Use when you don't need to control the value of the rating. | |
disabled | booleanWhether the rating is disabled. | |
form | stringThe associate form of the underlying input element. | |
id | stringThe unique identifier of the machine. | |
ids | Partial<{
root: string
label: string
hiddenInput: string
control: string
item: (id: string) => string
}>The ids of the elements in the rating. Useful for composition. | |
name | stringThe name attribute of the rating element (used in forms). | |
onHoverChange | (details: HoverChangeDetails) => voidFunction to be called when the rating value is hovered. | |
onValueChange | (details: ValueChangeDetails) => voidFunction to be called when the rating value changes. | |
readOnly | booleanWhether the rating is readonly. | |
required | booleanWhether the rating is required. | |
translations | IntlTranslationsSpecifies the localized strings that identifies the accessibility elements and their states | |
value | numberThe controlled value of the rating |
Item
| Prop | Default | Type |
|---|---|---|
index* | number | |
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. |