Lattice components

Radio Group

Single-selection primitive that owns the chosen value, registers items in order, and moves selection across them with gamepad and arrow-key navigation.

@lattice-ui/radio-group Stable direction import RadioGroup depends on core , focus , layer , motion

Radio Group is the primitive for picking exactly one option from a set: difficulty selectors, team pickers, quality settings, and any mutually-exclusive choice. Root owns the selected value; Item registers itself in the group, reflects whether it is checked, and handles activation and directional navigation; Indicator reveals a checkmark on the selected item.

Reach for Radio Group when a choice is single-select, the options should be navigable as a unit with the arrow keys or a gamepad, and selection should follow focus the way native radio groups do.

Import

import { RadioGroup } from "@lattice-ui/radio-group";

Anatomy

Wrap a set of Items in a Root. Each Item carries a value and may contain an Indicator that shows only while that item is checked.

RadioGroup anatomy
<RadioGroup.Root>
<RadioGroup.Item value="...">
<RadioGroup.Indicator />
</RadioGroup.Item>
</RadioGroup.Root>
PartRequiredResponsibility
RadioGroup.RootyesOwns the selected value, group state, item registry, and directional selection movement.
RadioGroup.ItemyesA selectable button bound to a value; activates, registers as a focus node, and drives arrow navigation.
RadioGroup.IndicatornoPresence-driven marker rendered only while its parent item is checked.

Example

A vertical difficulty picker driven by app state, with a checkmark indicator inside each item.

DifficultyPicker.tsx
import { useState } from "@rbxts/react";
import { RadioGroup } from "@lattice-ui/radio-group";
const OPTIONS = ["Easy", "Normal", "Hard"] as const;
export function DifficultyPicker() {
const [difficulty, setDifficulty] = useState("Normal");
return (
<RadioGroup.Root value={difficulty} onValueChange={setDifficulty} orientation="vertical">
<uilistlayout Padding={new UDim(0, 8)} SortOrder={Enum.SortOrder.LayoutOrder} />
{OPTIONS.map((option) => (
<RadioGroup.Item key={option} value={option}>
<uilistlayout
FillDirection={Enum.FillDirection.Horizontal}
Padding={new UDim(0, 8)}
VerticalAlignment={Enum.VerticalAlignment.Center}
/>
<frame
BackgroundColor3={Color3.fromRGB(20, 22, 28)}
Size={UDim2.fromOffset(20, 20)}
>
<uicorner CornerRadius={new UDim(1, 0)} />
<RadioGroup.Indicator>
<frame
AnchorPoint={new Vector2(0.5, 0.5)}
BackgroundColor3={Color3.fromRGB(240, 244, 252)}
Position={UDim2.fromScale(0.5, 0.5)}
Size={UDim2.fromOffset(10, 10)}
>
<uicorner CornerRadius={new UDim(1, 0)} />
</frame>
</RadioGroup.Indicator>
</frame>
</RadioGroup.Item>
))}
</RadioGroup.Root>
);
}

How it behaves

Value and selection

RadioGroup.Root is controllable: pass value and onValueChange to drive it, or defaultValue to run uncontrolled. An Item is checked when its value equals the group value. Activating an item (click, Return, Space, or gaining gamepad selection) calls setValue with that item’s value. When the group or the item is disabled, activation is ignored.

Orientation and navigation

Root takes an orientation of "vertical" (default) or "horizontal". Each Item registers as a focus node and listens for arrow keys: Up/Down move selection in a vertical group, Left/Right in a horizontal one. Movement walks the ordered item registry, focuses the next item, and selects it in the same step — so selection follows focus, matching native radio behavior. disabled items are skipped by focus.

Item registration order

Items register themselves with Root on mount and unregister on unmount, each carrying a monotonically increasing order. Directional navigation resolves the next item from this ordered registry, so navigation order tracks mount order rather than visual position. Lay items out with a uilistlayout whose order matches mount order to keep navigation intuitive.

Indicator motion

RadioGroup.Indicator is presence-driven: it mounts a reveal recipe from @lattice-ui/motion when its item becomes checked and runs an exit animation when unchecked before unmounting. Pass transition to override the recipe. Set forceMount to keep the indicator mounted through its exit (driving visibility yourself), in which case it bypasses the presence wrapper and stays rendered. The default marker is a small light frame; with asChild, your child element is rendered and its visibility is bound to the presence state.

Default item rendering

Without asChild, an Item renders a textbutton whose Text is the item’s value, tinted by selection (it animates BackgroundColor3 between an active and inactive accent via the selection response recipe; override with transition). Use asChild to project the item behavior onto your own element via the core Slot, keeping the activation, selection, and arrow-key wiring.

API reference

RadioGroup.Root

Prop Type Description
value string Controlled selected value. Pair with onValueChange.
defaultValue string Initial selected value for uncontrolled usage.
onValueChange (value: string) => void Called when the selected value changes (never with undefined).
disabled boolean Disables the whole group; activation and selection are ignored. Defaults to false.
required boolean Marks selection as required. Exposed on context for consumers. Defaults to false.
orientation "horizontal" | "vertical" Axis for arrow-key navigation. Defaults to "vertical".
children React.ReactNode The radio items.

RadioGroup.Item

Prop Type Description
value string The value this item represents. Required; used to compare against the group value.
disabled boolean Disables this item; it is skipped by navigation and cannot be selected. Defaults to false.
transition ResponseMotionConfig Overrides the default selection response recipe used to tint the item by checked state.
asChild boolean Project item behavior onto your own element via Slot instead of the default textbutton.
children React.ReactElement The element to render (with asChild) or content placed inside the default button, such as an Indicator.

RadioGroup.Indicator

Prop Type Description
transition PresenceMotionConfig Overrides the default reveal/exit recipe for showing and hiding the marker.
forceMount boolean Keeps the indicator mounted regardless of presence, bypassing the exit animation wrapper. Defaults to false.
asChild boolean Render your own marker element instead of the default frame; its visibility is bound to presence.
children React.ReactNode The marker content to render.