@lattice-ui/react-toggle-groupStable directionimport ToggleGroupdepends on runtime, focus, motionToggle Group is for a related set of toggles sharing selection state: view switches, filter chips,
alignment pickers. Root owns the value and tells each Item whether it is pressed, so your items
only render their two visual states. It is single-select, where picking one clears the others,
or multiple-select, where each toggles independently. For a lone on/off control, use
Checkbox or Switch.
Preview
import { React } from "@lattice-ui/react-runtime";import { Text, useTheme } from "@lattice-ui/react-style";import { ToggleGroup } from "@lattice-ui/react-toggle-group";
function ToggleGroupExample() { const { theme } = useTheme(); const [formats, setFormats] = React.useState<Array<string>>(["bold"]); const [heading, setHeading] = React.useState("h1");
const formatItems: Array<{ label: string; value: string; font: Enum.Font }> = [ { label: "B", value: "bold", font: Enum.Font.GothamBold }, { label: "I", value: "italic", font: Enum.Font.Gotham }, { label: "U", value: "underline", font: Enum.Font.Gotham }, ];
const headingItems: Array<{ label: string; value: string }> = [ { label: "H1", value: "h1" }, { label: "H2", value: "h2" }, ];
const renderItem = (label: string, font: Enum.Font, pressed: boolean, layoutOrder: number) => ( <textbutton AutoButtonColor={false} BackgroundColor3={theme.colors.accent} BackgroundTransparency={pressed ? 0 : 1} BorderSizePixel={0} LayoutOrder={layoutOrder} Size={UDim2.fromOffset(34, 34)} Text="" > <uicorner CornerRadius={new UDim(0, theme.radius.sm)} /> <Text BackgroundTransparency={1} Font={font} Size={UDim2.fromScale(1, 1)} Text={label} TextColor3={pressed ? theme.colors.accentContrast : theme.colors.textSecondary} TextSize={theme.typography.labelSm.textSize} /> </textbutton> );
return ( <frame BackgroundColor3={theme.colors.surfaceElevated} BorderSizePixel={0} Size={UDim2.fromScale(1, 1)}> <uicorner CornerRadius={new UDim(0, theme.radius.lg)} /> <uistroke Color={theme.colors.border} Thickness={1} /> <uipadding PaddingBottom={new UDim(0, theme.space[6])} PaddingLeft={new UDim(0, theme.space[6])} PaddingRight={new UDim(0, theme.space[6])} PaddingTop={new UDim(0, theme.space[6])} /> <uilistlayout FillDirection={Enum.FillDirection.Horizontal} Padding={new UDim(0, theme.space[6])} VerticalAlignment={Enum.VerticalAlignment.Center} />
<ToggleGroup.Root asChild onValueChange={setFormats} type="multiple" value={formats}> <frame BackgroundTransparency={1} LayoutOrder={0} Size={UDim2.fromOffset(106, 34)}> <uilistlayout FillDirection={Enum.FillDirection.Horizontal} Padding={new UDim(0, theme.space[2])} /> {formatItems.map((item, index) => ( <ToggleGroup.Item asChild key={item.value} value={item.value}> {renderItem(item.label, item.font, formats.includes(item.value), index)} </ToggleGroup.Item> ))} </frame> </ToggleGroup.Root>
<frame BackgroundColor3={theme.colors.border} BorderSizePixel={0} LayoutOrder={1} Size={UDim2.fromOffset(1, 22)} />
<ToggleGroup.Root asChild onValueChange={(nextValue) => setHeading(nextValue ?? "")} type="single" value={heading} > <frame BackgroundTransparency={1} LayoutOrder={2} Size={UDim2.fromOffset(70, 34)}> <uilistlayout FillDirection={Enum.FillDirection.Horizontal} Padding={new UDim(0, theme.space[2])} /> {headingItems.map((item, index) => ( <ToggleGroup.Item asChild key={item.value} value={item.value}> {renderItem(item.label, Enum.Font.GothamMedium, heading === item.value, index)} </ToggleGroup.Item> ))} </frame> </ToggleGroup.Root> </frame> );}Import
import { ToggleGroup } from "@lattice-ui/react-toggle-group";Anatomy
A Root wrapping one Item per option. Root requires a type that fixes the whole group as single- or multiple-select.
ToggleGroup anatomy
<ToggleGroup.Root type="single"> <ToggleGroup.Item value="..." /> <ToggleGroup.Item value="..." /></ToggleGroup.Root>| Part | Required | Responsibility |
|---|---|---|
ToggleGroup.Root | yes | Owns the selected value(s), the select mode, and group-wide disabled state. |
ToggleGroup.Item | yes | One toggle; reads its pressed state from Root. |
Examples
Single-select toolbar
An uncontrolled single-select picker seeded with defaultValue. Activating the already-pressed item
clears the selection and onValueChange fires with undefined, so a toolbar like this can be fully
deselected.
import { ToggleGroup } from "@lattice-ui/react-toggle-group";
export function HotbarModePicker() { return ( <ToggleGroup.Root type="single" defaultValue="build" onValueChange={(mode) => print(`active mode: ${mode}`)} > <uilistlayout FillDirection={Enum.FillDirection.Horizontal} Padding={new UDim(0, 6)} /> <ToggleGroup.Item value="build" /> <ToggleGroup.Item value="terrain" /> <ToggleGroup.Item value="wire" /> </ToggleGroup.Root> );}Multiple-select filters
With type="multiple" the value is a string[]: each item toggles independently and the array
preserves selection order. onValueChange receives the full next array on every toggle.
import { ToggleGroup } from "@lattice-ui/react-toggle-group";
export function ItemFilters() { return ( <ToggleGroup.Root type="multiple" defaultValue={["new"]} onValueChange={(filters) => print(`active filters: ${filters.join(", ")}`)} > <uilistlayout FillDirection={Enum.FillDirection.Horizontal} Padding={new UDim(0, 6)} /> <ToggleGroup.Item value="new" /> <ToggleGroup.Item value="owned" /> <ToggleGroup.Item value="tradable" /> </ToggleGroup.Root> );}Controlled group
Pass value and onValueChange when something outside reads or sets the selection. In single mode
the controlled value is string | undefined, so design around the empty case.
import { useState } from "@rbxts/react";import { ToggleGroup } from "@lattice-ui/react-toggle-group";
export function DifficultyPicker(props: { onStart: (difficulty: string) => void }) { const [difficulty, setDifficulty] = useState<string | undefined>("normal");
return ( <frame BackgroundTransparency={1} Size={UDim2.fromOffset(320, 90)}> <uilistlayout Padding={new UDim(0, 10)} SortOrder={Enum.SortOrder.LayoutOrder} />
<ToggleGroup.Root type="single" value={difficulty} onValueChange={setDifficulty}> <uilistlayout FillDirection={Enum.FillDirection.Horizontal} Padding={new UDim(0, 6)} /> <ToggleGroup.Item value="casual" /> <ToggleGroup.Item value="normal" /> <ToggleGroup.Item value="hardcore" /> </ToggleGroup.Root>
<textbutton Active={difficulty !== undefined} AutoButtonColor={difficulty !== undefined} BackgroundColor3={ difficulty !== undefined ? Color3.fromRGB(88, 142, 255) : Color3.fromRGB(59, 66, 84) } Event={{ Activated: () => { if (difficulty !== undefined) { props.onStart(difficulty); } }, }} Size={UDim2.fromOffset(160, 32)} Text={difficulty !== undefined ? `Start (${difficulty})` : "Pick a difficulty"} TextColor3={Color3.fromRGB(240, 244, 252)} /> </frame> );}Disabled group and disabled items
disabled works at two levels. On Root it freezes the whole control, short-circuiting the toggle
logic so nothing can change the value. On an Item it disables just that option.
import { ToggleGroup } from "@lattice-ui/react-toggle-group";
export function LoadoutModes(props: { inMatch: boolean; hasPremium: boolean }) { return ( <ToggleGroup.Root type="single" defaultValue="assault" disabled={props.inMatch}> <uilistlayout FillDirection={Enum.FillDirection.Horizontal} Padding={new UDim(0, 6)} /> <ToggleGroup.Item value="assault" /> <ToggleGroup.Item value="recon" /> <ToggleGroup.Item value="specialist" disabled={!props.hasPremium} /> </ToggleGroup.Root> );}A disabled item is Active={false} but still renders its pressed state, so a locked-in selection
stays visibly selected.
Custom pressed visuals with asChild
Use asChild on Root for a different container class, and on each Item to project the toggle
onto your own button. The primitive never writes an item’s colours — read the pressed state from
Root and set them yourself.
import { useState } from "@rbxts/react";import { ToggleGroup } from "@lattice-ui/react-toggle-group";
const VIEWS = ["grid", "list", "detail"];
export function StyledViewSwitch() { const [view, setView] = useState<string | undefined>("grid");
return ( <ToggleGroup.Root type="single" value={view} onValueChange={setView} asChild> <frame BackgroundColor3={Color3.fromRGB(24, 26, 32)} BorderSizePixel={0} Size={UDim2.fromOffset(248, 44)} > <uicorner CornerRadius={new UDim(0, 8)} /> <uilistlayout FillDirection={Enum.FillDirection.Horizontal} HorizontalAlignment={Enum.HorizontalAlignment.Center} Padding={new UDim(0, 6)} VerticalAlignment={Enum.VerticalAlignment.Center} />
{VIEWS.map((viewName) => ( <ToggleGroup.Item value={viewName} key={viewName} asChild> <textbutton AutoButtonColor={false} Size={UDim2.fromOffset(76, 32)} Text={viewName} TextSize={14}> <uicorner CornerRadius={new UDim(0, 6)} /> <uistroke Color={Color3.fromRGB(88, 142, 255)} Thickness={view === viewName ? 1 : 0} /> </textbutton> </ToggleGroup.Item> ))} </frame> </ToggleGroup.Root> );}ToggleGroup.Item accepts no transition. Animate your own colours if you want the change to ease.
How it behaves
Value state
Pass value and onValueChange to control the selection, or defaultValue to let the group own
it. onValueChange fires either way. Each Item reads its pressed state from context and routes
activation through Root.
Single vs multiple
The required type prop fixes the group’s behavior and the shape of its value:
type="single"— the value is astring, andonValueChangereceivesstring | undefined. Selecting replaces the current value. Selecting the already-selected item clears it.type="multiple"— the value is astring[], appended in selection order, with duplicate and non-string entries stripped from incoming arrays.
The value type and onValueChange signature are a discriminated union over type, so set type as
a literal for TypeScript to narrow.
Activation and selection
ToggleGroup.Item toggles on Activated and on Return/Space. It renders an unstyled
textbutton with AutoButtonColor off and does not render value as its text. Items render
with Selectable={false}, so manage gamepad selection at the surrounding layout.
Under asChild the slot’s Active, Selectable and motion ref win over the child’s props, and
event handlers compose. Use a textbutton or imagebutton so Activated fires.
Group container
Root renders a transparent frame with no size of its own — a context boundary, not a laid-out
surface. Put a uilistlayout inside it and pass a Size so it reserves space in a flow layout.
Disabled state
disabled on Root disables the group, on an Item just that one. A disabled item ignores
activation and keyboard toggles and is Active={false} but keeps rendering its pressed visuals.
Group-level disabled also short-circuits Root’s toggle logic, so nothing gets through.
Item colors
An item’s BackgroundColor3 and TextColor3 are entirely yours — the primitive writes neither, in
either mode. Read the pressed state from ToggleGroup.Root and branch on it, adding your own
response motion if you want the change eased.
Activating the pressed item in a single-select group deselects it — there is no “always keep one selected” mode. If your UI requires a selection, run controlled and ignore the clear.
The Root frame is transparent and unsized, so it reserves no space in a uilistlayout or uigridlayout parent. Pass a Size, or asChild with your own container.
The item writes no colours and accepts no transition. Colours you set stay exactly as you set them.
API reference
ToggleGroup.Root
Root takes the common props below plus the single- or multiple-mode props selected by type.
| Prop | Type | Description |
|---|---|---|
| type | "single" | "multiple" | Required. Fixes the group as single- or multiple-select and determines the value shape. Pass a literal so TypeScript narrows the union. |
| value | string | string[] | Controlled selection. string in single mode, string[] (normalized: duplicates and non-strings stripped) in multiple mode. Pair with onValueChange. |
| defaultValue | string | string[] | Initial selection for uncontrolled usage. string in single mode; string[] (default []) in multiple mode. |
| onValueChange | (value: string | undefined) => void | (value: string[]) => void | Called on every selection change, controlled or not. Receives string | undefined in single mode and the full next string[] in multiple mode. |
| disabled | boolean | Disables every item and short-circuits the group's toggle logic so no value change gets through. Defaults to false. |
| asChild | boolean | Render the single child element as the group container instead of the frame the part renders. |
| children | React.ReactNode | The toggle items and any layout. Must be a single valid element when asChild is set. |
ToggleGroup.Item
| Prop | Type | Description |
|---|---|---|
| value | string | Required. Identifies this item within the group. Since 0.7.0 it is not rendered as the button's text — pass Text yourself. |
| disabled | boolean | Disables just this item — activation and keyboard toggles are ignored and the button is marked inactive. Defaults to false. |
| asChild | boolean | Merge the toggle behavior onto the single child element instead of the textbutton the part renders. The child must be an activatable button. |
| children | React.ReactNode | Rendered inside the item button, or the element to project onto. Must be a single element when asChild is set. |