Rating Group
Star-based rating input component supporting controlled/uncontrolled modes, half-star ratings, and disabled/readonly states.
import { RatingGroup, RatingGroupLabel } from "~/components/rating-group";
export default function RatingGroupBasicDemo() {
return (
<RatingGroup count={5} defaultValue={3}>
<RatingGroupLabel>Rate this</RatingGroupLabel>
</RatingGroup>
);
}Installation
CLI
Run the following command to add the component to your project:
npx @ark-preset/cli@latest add rating-groupManual
Create the recipe file at src/components/recipes/rating-group.ts:
import { tv, type VariantProps } from "tailwind-variants";
export const ratingGroupVariants = tv({
slots: {
root: "flex items-center gap-2",
control: "flex items-center gap-0.5",
item: [
"group cursor-pointer transition duration-300 motion-safe:hover:-translate-y-0.5 motion-safe:active:translate-y-0.5 active:duration-150 ease-out",
"data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed",
"data-[readonly]:cursor-default",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-sm",
],
itemIndicator: "relative inline-flex items-center justify-center",
},
variants: {
size: {
sm: {
item: "size-4",
},
md: {
item: "size-5",
},
lg: {
item: "size-6",
},
},
orientation: {
horizontal: {
root: "flex-row items-center gap-2",
},
vertical: {
root: "flex-col items-start gap-1",
},
},
},
defaultVariants: {
size: "md",
orientation: "horizontal",
},
});
export type RatingGroupVariants = VariantProps<typeof ratingGroupVariants>;Create the component directory and files.
src/components/rating-group/rating-group.base.tsx:
import { RatingGroup as ArkRatingGroup } from "@ark-ui/solid/rating-group";
import { createContext, useContext, splitProps, type Component } from "solid-js";
import { ratingGroupVariants, labelVariants, type RatingGroupVariants } from "../recipes/rating-group";
type RatingGroupVariantContextValue = Pick<RatingGroupVariants, "size" | "orientation">;
const RatingGroupVariantContext = createContext<RatingGroupVariantContextValue>();
const useRatingGroupVariant = () => useContext(RatingGroupVariantContext);
const styles = ratingGroupVariants();
const Root: Component<ArkRatingGroup.RootProps & RatingGroupVariants> = (props) => {
const [local, others] = splitProps(props, ["class", "size", "orientation"]);
return (
<RatingGroupVariantContext.Provider
value={{ size: local.size, orientation: local.orientation }}
>
<ArkRatingGroup.Root
class={styles.root({
class: local.class,
size: local.size,
orientation: local.orientation,
})}
{...others}
/>
</RatingGroupVariantContext.Provider>
);
};
const RootProvider: Component<ArkRatingGroup.RootProviderProps & RatingGroupVariants> = (props) => {
const [local, others] = splitProps(props, ["class", "size", "orientation"]);
return (
<RatingGroupVariantContext.Provider
value={{ size: local.size, orientation: local.orientation }}
>
<ArkRatingGroup.RootProvider
class={styles.root({
class: local.class,
size: local.size,
orientation: local.orientation,
})}
{...others}
/>
</RatingGroupVariantContext.Provider>
);
};
const Label: Component<ArkRatingGroup.LabelProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkRatingGroup.Label class={labelVariants({ class: local.class })} {...others} />;
};
const Control: Component<ArkRatingGroup.ControlProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkRatingGroup.Control class={styles.control({ class: local.class })} {...others} />;
};
const Item: Component<ArkRatingGroup.ItemProps & RatingGroupVariants> = (props) => {
const ctx = useRatingGroupVariant();
const [local, others] = splitProps(props, ["class", "size", "orientation"]);
return (
<ArkRatingGroup.Item
class={styles.item({
class: local.class,
size: local.size ?? ctx?.size,
orientation: local.orientation ?? ctx?.orientation,
})}
{...others}
/>
);
};
const ItemContext = ArkRatingGroup.ItemContext;
const Context = ArkRatingGroup.Context;
const HiddenInput = ArkRatingGroup.HiddenInput;
export const RatingGroup = {
Root,
RootProvider,
Label,
Control,
Item,
ItemContext,
Context,
HiddenInput,
};
export { RatingGroupVariantContext, useRatingGroupVariant };src/components/rating-group/index.tsx:
import { Index, splitProps, type Component } from "solid-js";
import { RatingGroup as RatingGroupBase } from "./rating-group.base";
import { RatingGroup as ArkRatingGroup } from "@ark-ui/solid/rating-group";
import { ratingGroupVariants, type RatingGroupVariants } from "../recipes/rating-group";
const styles = ratingGroupVariants();
const RatingGroupItem: Component<ArkRatingGroup.ItemProps & RatingGroupVariants> = (props) => {
const [local, others] = splitProps(props, ["index", "class", "size"]);
return (
<RatingGroupBase.Item
index={local.index}
class={styles.item({ class: local.class, size: local.size })}
{...others}
>
<RatingGroupBase.ItemContext>
{() => (
<span class={styles.itemIndicator()}>
{/* Background unfilled star */}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-full"
>
<polygon 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>
{/* Foreground filled star (hidden when not highlighted, clip-path for half) */}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="absolute inset-0 size-full opacity-0 group-data-[highlighted]:opacity-100 group-data-[half]:[clip-path:inset(0_50%_0_0)]"
>
<polygon 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>
</span>
)}
</RatingGroupBase.ItemContext>
</RatingGroupBase.Item>
);
};
const RatingGroup: Component<ArkRatingGroup.RootProps & RatingGroupVariants> = (props) => {
const [local, others] = splitProps(props, ["size", "orientation", "children"]);
return (
<RatingGroupBase.Root size={local.size} orientation={local.orientation} {...others}>
{local.children}
<RatingGroupBase.Context>
{(context) => (
<RatingGroupBase.Control>
<Index each={context().items}>{(item) => <RatingGroupItem index={item()} />}</Index>
<RatingGroupBase.HiddenInput />
</RatingGroupBase.Control>
)}
</RatingGroupBase.Context>
</RatingGroupBase.Root>
);
};
const RatingGroupLabel: Component<ArkRatingGroup.LabelProps> = (props) => {
return <RatingGroupBase.Label {...props} />;
};
const RatingGroupControl: Component<ArkRatingGroup.ControlProps> = (props) => {
return <RatingGroupBase.Control {...props} />;
};
const RatingGroupContext: Component<ArkRatingGroup.ContextProps> = (props) => {
return <RatingGroupBase.Context {...props} />;
};
const RatingGroupHiddenInput: Component<ArkRatingGroup.HiddenInputProps> = (props) => {
return <RatingGroupBase.HiddenInput {...props} />;
};
export {
RatingGroup,
RatingGroupItem,
RatingGroupLabel,
RatingGroupControl,
RatingGroupContext,
RatingGroupHiddenInput,
RatingGroupBase,
};
export { ratingGroupVariants, type RatingGroupVariants } from "../recipes/rating-group";Note: Make sure your project has the Tailwind CSS theme variables set up (
--background,--foreground,--ring,--border, etc.) or override the utility classes to match your design system.
Usage
Basic Usage
import { RatingGroup, RatingGroupLabel } from "~/components/rating-group";
export default function RatingGroupBasicDemo() {
return (
<RatingGroup count={5} defaultValue={3}>
<RatingGroupLabel>Rate this</RatingGroupLabel>
</RatingGroup>
);
}Controlled
Use value and onValueChange to control the rating value externally:
Value: 3
import { createSignal } from "solid-js";
import { RatingGroup } from "~/components/rating-group";
export default function RatingGroupControlledDemo() {
const [value, setValue] = createSignal(3);
return (
<div>
<p class="text-sm text-muted-foreground mb-2">Value: {value()}</p>
<RatingGroup count={5} value={value()} onValueChange={(e: any) => setValue(e.value)} />
</div>
);
}Half Star
Allow 0.5 value steps by setting the allowHalf prop to true:
import { RatingGroup, RatingGroupLabel } from "~/components/rating-group";
export default function RatingGroupHalfStarDemo() {
return (
<RatingGroup count={5} defaultValue={2.5} allowHalf>
<RatingGroupLabel>Rate this</RatingGroupLabel>
</RatingGroup>
);
}Disabled
To make the rating group disabled, set the disabled prop to true:
import { RatingGroup, RatingGroupLabel } from "~/components/rating-group";
export default function RatingGroupDisabledDemo() {
return (
<RatingGroup count={5} defaultValue={4} disabled>
<RatingGroupLabel>Rate this</RatingGroupLabel>
</RatingGroup>
);
}Advanced Usage
When the composite RatingGroup doesn’t provide enough control, import the raw primitive parts from the base file directly:
import { RatingGroup, useRatingGroupVariant } from "~/components/rating-group/rating-group.base";Or import RatingGroupBase (the raw parts namespace) from the composite entry point:
import { RatingGroupBase } from "~/components/rating-group";RootProvider Pattern
For full control over the rating group machine, use useRatingGroup with RatingGroup.RootProvider:
RootProvider: 3
import { useRatingGroup } from "@ark-ui/solid/rating-group";
import { RatingGroupBase } from "~/components/rating-group";
import { Index } from "solid-js";
export default function RatingGroupRootProviderDemo() {
const ratingGroup = useRatingGroup({ count: 5, defaultValue: 3 });
return (
<div>
<p class="text-sm text-muted-foreground mb-2">RootProvider: {ratingGroup().value}</p>
<RatingGroupBase.RootProvider value={ratingGroup}>
<RatingGroupBase.Label>Rate this</RatingGroupBase.Label>
<RatingGroupBase.Context>
{(context: any) => (
<RatingGroupBase.Control>
<Index each={context().items}>
{(item) => (
<RatingGroupBase.Item index={item()}>
<RatingGroupBase.ItemContext>
{() => (
<span class="relative inline-flex items-center justify-center size-5">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-full"
>
<polygon 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>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="absolute inset-0 size-full opacity-0 group-data-[highlighted]:opacity-100 group-data-[half]:[clip-path:inset(0_50%_0_0)]"
>
<polygon 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>
</span>
)}
</RatingGroupBase.ItemContext>
</RatingGroupBase.Item>
)}
</Index>
<RatingGroupBase.HiddenInput />
</RatingGroupBase.Control>
)}
</RatingGroupBase.Context>
</RatingGroupBase.RootProvider>
</div>
);
}API Reference
See the Ark UI RatingGroup documentation.