All components
Rating Group
Star rating input with keyboard-accessible items across four sizes.
12345678910111213import { RatingGroup } from "#components/ui/base-ui/rating-group.tsx";import { indigoPalette } from "@stylexblocks/ui/themes/palettes.ts";
export function Example() { // Base UI ships no rating machine — the component composes the recipe with // Base UI's useRender/mergeProps utilities around an accessible radio group return ( <RatingGroup count={5} defaultValue={3} size="md" palette={indigoPalette} aria-label="Rating" /> );}
// controlled: <RatingGroup value={value} onValueChange={setValue} />// composition: <RatingGroup render={<section />} />Sizes
xs
sm
md
lg
Install
1npx stylexblocks add rating-groupSource
components/ui/base-ui/rating-group.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195import * as stylex from "@stylexjs/stylex";import { useState } from "react";import { mergeProps } from "@base-ui-components/react/merge-props";import { useRender } from "@base-ui-components/react/use-render";import { colorPalette } from "@stylexblocks/ui/color-palette.stylex.ts";import { semanticColors } from "@stylexblocks/ui/semantic-tokens/colors.stylex.ts";import type { StyleOverrideProps } from "#lib/recipe-props.ts";import { ratingGroupItemIndicatorIconVars } from '@stylexblocks/ui/slot-recipes/rating-group.stylex.ts'
import { fontSizes } from '@stylexblocks/ui/tokens/font-sizes.stylex.ts'import { lineHeights } from '@stylexblocks/ui/tokens/line-heights.stylex.ts'
const ratingGroupRoot = stylex.create({ base: { display: 'inline-flex', },})
const ratingGroupControl = stylex.create({ base: { display: 'inline-flex', alignItems: 'center', },})
const ratingGroupItem = stylex.create({ base: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', userSelect: 'none', },})
const ratingGroupItemSizes = stylex.create({ xs: { fontSize: fontSizes.sm, lineHeight: lineHeights.sm, }, sm: { fontSize: fontSizes.md, lineHeight: lineHeights.md, }, md: { fontSize: fontSizes.xl, lineHeight: lineHeights.xl, }, lg: { fontSize: fontSizes._2xl, lineHeight: lineHeights._2xl, },})
const ratingGroupItemIndicator = stylex.create({ base: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '1em', height: '1em', position: 'relative', [ratingGroupItemIndicatorIconVars.stroke]: 'currentColor', [ratingGroupItemIndicatorIconVars.width]: '100%', [ratingGroupItemIndicatorIconVars.height]: '100%', [ratingGroupItemIndicatorIconVars.display]: 'inline-block', [ratingGroupItemIndicatorIconVars.flexShrink]: 0, [ratingGroupItemIndicatorIconVars.position]: 'absolute', [ratingGroupItemIndicatorIconVars.left]: 0, [ratingGroupItemIndicatorIconVars.top]: 0, },})
const ratingGroupItemIndicatorIcon = stylex.create({ base: { stroke: ratingGroupItemIndicatorIconVars.stroke, width: ratingGroupItemIndicatorIconVars.width, height: ratingGroupItemIndicatorIconVars.height, display: ratingGroupItemIndicatorIconVars.display, flexShrink: ratingGroupItemIndicatorIconVars.flexShrink, position: ratingGroupItemIndicatorIconVars.position, left: ratingGroupItemIndicatorIconVars.left, top: ratingGroupItemIndicatorIconVars.top, },})
export type RatingGroupSize = keyof typeof ratingGroupItemSizes
// Base UI ships no rating machine — this component composes the recipe with// Base UI's useRender/mergeProps utilities around controlled radio state.const ratingLocal = stylex.create({ starFilled: { color: colorPalette.solid }, starEmpty: { color: semanticColors.borderEmphasized }, btnReset: { backgroundColor: "transparent", borderWidth: 0, margin: 0, padding: 0, color: "inherit", fontFamily: "inherit", cursor: "pointer", },});
function StarIcon({ filled }: { filled: boolean }) { return ( <svg viewBox="0 0 24 24" fill={filled ? "currentColor" : "none"} strokeWidth="1.5" aria-hidden="true" {...stylex.props(ratingGroupItemIndicatorIcon.base)} > <path d="M12 3.2 14.7 8.8l6.1.9-4.4 4.3 1 6.1-5.4-2.9-5.4 2.9 1-6.1L3.2 9.7l6.1-.9L12 3.2Z" /> </svg> );}
export interface RatingGroupProps extends Omit<useRender.ComponentProps<"div">, "style" | "defaultValue">, StyleOverrideProps { size?: RatingGroupSize; /** Number of rating items. */ count?: number; value?: number; defaultValue?: number; onValueChange?: (value: number) => void;}
/** * Rating Group — the StyleX slot recipe composed with Base UI's * useRender/mergeProps utilities and an accessible radio-group pattern. */export function RatingGroup({ render, size = "md", count = 5, value, defaultValue = 0, onValueChange, palette, style, ...props}: RatingGroupProps) { const [inner, setInner] = useState(defaultValue); const current = value ?? inner; return useRender({ render, defaultTagName: "div", state: { value: current }, props: mergeProps<"div">( { role: "radiogroup", ...stylex.props(palette, ratingGroupRoot.base, style), children: ( <div {...stylex.props(ratingGroupControl.base)}> {Array.from({ length: count }, (_, index) => { const star = index + 1; const filled = star <= current; return ( <button key={star} type="button" role="radio" aria-checked={current === star} aria-label={`${star} star${star === 1 ? "" : "s"}`} onClick={() => { setInner(star); onValueChange?.(star); }} {...stylex.props( ratingLocal.btnReset, ratingGroupItem.base, ratingGroupItemSizes[size], )} > <span {...stylex.props( ratingGroupItemIndicator.base, filled ? ratingLocal.starFilled : ratingLocal.starEmpty, )} > <StarIcon filled={filled} /> </span> </button> ); })} </div> ), }, props, ), });}