Rating Group
A row of stars for scoring something.
import { Div, signal, Span } from "@implementjs/core";
import { RatingGroup, RatingGroupItem } from "@/lib/components/ui/rating-group";
export default function RatingGroupDemo() {
const value = signal(3);
return Div(
{ class: "flex flex-col items-center gap-2" },
RatingGroup(
{ value, "aria-label": "Rate this library" },
...Array.from({ length: 5 }, (_, index) => RatingGroupItem({ index })),
),
Span(
{ class: "text-sm text-muted-foreground tabular-nums" },
value.bind((v) => `${v} out of 5`),
),
);
}Installation
npx jsrepo add @implementjs/ui/rating-group
It installs @implementjs/lucide at the same time.
Copy the file below to src/lib/components/ui/rating-group.ts. It imports cn, so copy utils.ts to src/lib/utils.ts too. Then, on top of @implementjs/core and @implementjs/primitives:
npm install @implementjs/lucide
import { type Child, type ComponentProps } from "@implementjs/core";
import { StarIcon } from "@implementjs/lucide";
import {
RatingGroup as RatingGroupPrimitive,
RatingGroupItem as RatingGroupItemPrimitive,
} from "@implementjs/primitives";
import { createComponent } from "@implementjs/primitives";
import { cn } from "@/lib/utils";
export type RatingGroupProps = ComponentProps<typeof RatingGroupPrimitive>;
export type RatingGroupItemProps = ComponentProps<typeof RatingGroupItemPrimitive>;
export const RatingGroup = createComponent(function RatingGroup(
{ class: className, ...props }: RatingGroupProps,
...children: Child[]
) {
return RatingGroupPrimitive(
{
...props,
"data-slot": "rating-group",
class: cn(
"flex w-fit items-center gap-1 rounded-md outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50",
className,
),
},
...children,
);
});
export const RatingGroupItem = createComponent(function RatingGroupItem(
{ class: className, ...props }: RatingGroupItemProps,
...children: Child[]
) {
return RatingGroupItemPrimitive(
{
...props,
"data-slot": "rating-group-item",
class: cn(
"group/rating-item cursor-pointer text-muted-foreground transition-colors",
"data-[state=active]:text-primary",
"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50",
"data-[readonly]:cursor-default",
className,
),
},
...(children.length > 0
? children
: [
StarIcon({
"aria-hidden": true,
class: "size-5 group-data-[state=active]/rating-item:fill-current",
}),
]),
);
});Usage
Each item renders a star that fills while it is active — pass children to use a different mark. The items are indexed rather than valued, so a five-star rating is Array.from({ length: 5 }).
import { signal } from "@implementjs/core";
import { RatingGroup, RatingGroupItem } from "@/lib/components/ui/rating-group";
const value = signal(3);
RatingGroup(
{ value, "aria-label": "Rate this library" },
...Array.from({ length: 5 }, (_, index) => RatingGroupItem({ index })),
);
Read-only
readonly on the root shows a score without offering to change it — the cursor stays an arrow and the items stop responding, but the value is still announced:
RatingGroup({ value: 4, readonly: true, "aria-label": "Average rating" } /* items */);
API Reference
Every prop the styling does not consume is forwarded to the Rating Group primitive, so the tables below are the whole surface — the behavior props and the styling ones together.
RatingGroup
The root and the single focusable control. Announces as a slider: role="slider" with the aria value attributes. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
value | Signal<number> | number | 0 | The current rating. Pass a signal to control it from outside; a number seeds uncontrolled state. |
min | number | 0 | The lowest value the rating can take. |
max | number | 5 | The highest value the rating can take. |
allowHalf | boolean | false | Work in half steps: pointer position picks the half, arrows move by 0.5. |
readonly | boolean | false | The value can be read but not changed. |
disabled | Signal<boolean> | boolean | false | Prevents changes and removes the group from the Tab order. |
hoverPreview | boolean | true | Preview the value under the pointer before clicking. |
orientation | "horizontal" | "vertical" | "horizontal" | The axis pointer positions are measured along for half steps. |
required | boolean | false | Sets aria-required on the group. |
| Data attribute | Value |
|---|---|
[data-rating-group-root] | Present |
[data-orientation] | "horizontal" | "vertical" |
[data-disabled] | Present when disabled |
[data-readonly] | Present when readonly |
RatingGroupItem
One visual step. role="presentation" — the root carries the semantics. Fill it with an icon and style against data-state. Renders a star that fills while active, unless you pass children of your own. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
index* | number | — | Zero-based position; the item represents the rating index + 1. |
disabled | Signal<boolean> | boolean | false | Ignores pointer input on this item. |
| Data attribute | Value |
|---|---|
[data-rating-group-item] | Present |
[data-state] | "active" | "partial" | "inactive" |
[data-value] | The rating the item represents |
[data-orientation] | "horizontal" | "vertical" |
[data-disabled] | Present when disabled |
[data-readonly] | Present when readonly |