Radio Group
One choice out of several.
import { Div, Label } from "@implementjs/core";
import { RadioGroup, RadioGroupItem } from "@/lib/components/ui/radio-group";
function Option(value: string, label: string) {
return Div(
{ class: "flex items-center gap-2" },
RadioGroupItem({ id: value, value }),
Label({ for: value, class: "text-sm leading-none font-medium" }, label),
);
}
export default function RadioGroupDemo() {
return RadioGroup(
{ value: "comfortable", "aria-label": "Density" },
Option("default", "Default"),
Option("comfortable", "Comfortable"),
Option("compact", "Compact"),
);
}Installation
npx jsrepo add @implementjs/ui/radio-group
It installs @implementjs/lucide at the same time.
Copy the file below to src/lib/components/ui/radio-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 { Span, type Child, type ComponentProps } from "@implementjs/core";
import { CircleIcon } from "@implementjs/lucide";
import {
RadioGroup as RadioGroupPrimitive,
RadioGroupItem as RadioGroupItemPrimitive,
} from "@implementjs/primitives";
import { createComponent } from "@implementjs/primitives";
import { cn } from "@/lib/utils";
export type RadioGroupProps = ComponentProps<typeof RadioGroupPrimitive>;
export type RadioGroupItemProps = ComponentProps<typeof RadioGroupItemPrimitive>;
export const RadioGroup = createComponent(function RadioGroup(
{ class: className, ...props }: RadioGroupProps,
...children: Child[]
) {
return RadioGroupPrimitive(
{ ...props, "data-slot": "radio-group", class: cn("grid gap-3", className) },
...children,
);
});
export const RadioGroupItem = createComponent(function RadioGroupItem(
{ class: className, ...props }: RadioGroupItemProps,
...children: Child[]
) {
return RadioGroupItemPrimitive(
{
...props,
"data-slot": "radio-group-item",
class: cn(
"group/radio-item aspect-square size-4 shrink-0 rounded-full border border-input text-primary shadow-xs transition-[color,box-shadow] outline-none",
"dark:bg-input/30",
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
"disabled:cursor-not-allowed disabled:opacity-50",
"data-[state=checked]:border-primary",
className,
),
},
...(children.length > 0
? children
: [
Span(
{
"data-slot": "radio-group-indicator",
class:
"relative hidden size-full items-center justify-center group-data-[state=checked]/radio-item:flex",
},
CircleIcon({
"aria-hidden": true,
class:
"absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 fill-primary text-primary",
}),
),
]),
);
});Usage
The group is a vertical grid; each item renders its own dot unless you pass children. Roving focus, arrow-key navigation, and the single-selection invariant are the primitive's.
import { RadioGroup, RadioGroupItem } from "@/lib/components/ui/radio-group";
RadioGroup(
{ value: "comfortable", "aria-label": "Density" },
Div(
{ class: "flex items-center gap-2" },
RadioGroupItem({ id: "comfortable", value: "comfortable" }),
Label({ for: "comfortable", class: "text-sm font-medium" }, "Comfortable"),
),
);
Give each item an id and point a Label at it, the same as a checkbox — the item is a Button, not an input.
Controlled
Pass a signal as the group's value to read or set the choice from outside:
const density = signal<string | null>("comfortable");
RadioGroup({ value: density, "aria-label": "Density" } /* items */);
API Reference
Every prop the styling does not consume is forwarded to the Radio Group primitive, so the tables below are the whole surface — the behavior props and the styling ones together.
RadioGroup
The root. Owns which item is checked and the arrow-key focus movement. Sets role="radiogroup". Styled as a vertical grid with a gap between items. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
value | Signal<string | null> | string | null | null | The checked item. Pass a signal to control it from outside; a string seeds uncontrolled state. |
disabled | Signal<boolean> | boolean | false | Disables every item in the group. |
required | boolean | false | Sets aria-required on the group. |
loop | boolean | true | Whether arrow keys wrap from the last item back to the first. |
orientation | "horizontal" | "vertical" | "vertical" | Announced to assistive technology and set as data-orientation. |
| Data attribute | Value |
|---|---|
[data-radio-group-root] | Present |
[data-orientation] | "horizontal" | "vertical" |
[data-disabled] | Present when disabled |
RadioGroupItem
One option. Sets role="radio" and aria-checked; arrowing to it checks it. Give it a look and an indicator. Renders its own dot indicator unless you pass children of your own. Renders a Button; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
value* | string | — | Identifies the item. Must be unique within the group. |
disabled | Signal<boolean> | boolean | false | Prevents checking the item. Sets disabled and data-disabled. |
| Data attribute | Value |
|---|---|
[data-radio-group-item] | Present |
[data-state] | "checked" | "unchecked" |
[data-value] | The item's value |
[data-orientation] | "horizontal" | "vertical" |
[data-disabled] | Present when disabled |