Facetcomponents

Radio group

One value across a set of dials, mirroring half the state and leaving the rest to the primitive.

Terminal window
npx facet-rbxts add radio-group

Copies ui/radio-group.tsx, plus lib/utils.ts. Needs @facet-ui/react-variants, @lattice-ui/react-runtime@^0.8.0 and @lattice-ui/react-radio-group@^0.8.0.

import { RadioGroup, RadioGroupItem } from "../shared/ui/radio-group";
import { Label } from "../shared/ui/label";
<RadioGroup defaultValue="normal" onValueChange={setDifficulty}>
<frame className="flex-row items-center gap-2 w-fit h-fit">
<RadioGroupItem value="normal" />
<Label Text="Normal" />
</frame>
</RadioGroup>
Three items, one checked. The row around each dial is yours — Facet ships no RadioGroupLabel.

The parts

PartRendersClasses
RadioGroupFrameflex-col gap-2 w-fit h-fit
RadioGroupItemTextButtonsize-4 rounded-full border border-input transition duration-150

Two parts, not four. The indicator and the dot inside it are drawn by RadioGroupItem and take no props. Exporting them would cost two Luau registers and buy nothing. See the register limit.

Props

RadioGroup

PropTypeDescription
valuestringControlled value. Pass it with onValueChange.
defaultValuestringUncontrolled starting value.
onValueChange(value: string) => voidFires when a different item is checked.
disabledbooleanDisables every item in the group. An item can also disable itself.
orientation"horizontal" | "vertical"Goes to the primitive for keyboard and gamepad navigation, and adds flex-row to the frame when horizontal. Defaults to vertical.
classNameClassNameThreaded into the root recipe's className slot inside the component. A class written at a Vela-compiled call site never reaches it — see Overriding from the call site.

RadioGroupItem

PropTypeDescription
valuestringRequired. What this item sets the group to.
disabledbooleanDisables this item alone.
classNameClassNameThreaded into the item recipe's className slot inside the component.

There is no Text on either part — a radio button is a dial, and the words beside it are a Label you place yourself.

Half the state is mirrored, half is not

The mirrored-state rule applies to what this file styles by, and here that is only the border:

const checked = group.value === props.value;
<RadioGroupPrimitive.Item
className={radioGroupVariants.item({
className: cn(checked && "border-primary", disabled && "opacity-50", props.className),
})}

The dot inside needs no mirror: RadioGroup.Indicator mounts and unmounts its children from the primitive’s own state, so there is no class to flip.

A Facet context carries the group’s value down to each item, because Lattice’s own is private. It exists for exactly one boolean per item.

orientation does two different things

<frame
className={radioGroupVariants.root({
className: cn(props.orientation === "horizontal" && "flex-row", props.className),
})}

It goes to the primitive for arrow-key navigation and is read again here to swap the frame’s layout — two mechanisms sharing a prop. Nothing in Lattice lays this frame out, and nothing in the frame tells Lattice which key moves where.