Latticecomponents

Toggle Group

A set of toggles in single- or multiple-select mode, owning pressed state.

@lattice-ui/react-toggle-groupStable directionimport ToggleGroupdepends on runtime, focus, motion

Toggle 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

Edit

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 anatomy
<ToggleGroup.Root type="single">
<ToggleGroup.Item value="..." />
<ToggleGroup.Item value="..." />
</ToggleGroup.Root>
PartRequiredResponsibility
ToggleGroup.RootyesOwns the selected value(s), the select mode, and group-wide disabled state.
ToggleGroup.ItemyesOne 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.

HotbarModePicker.tsx
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.

ItemFilters.tsx
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.

DifficultyPicker.tsx
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.

LoadoutModes.tsx
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.

StyledViewSwitch.tsx
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 a string, and onValueChange receives string | undefined. Selecting replaces the current value. Selecting the already-selected item clears it.
  • type="multiple" — the value is a string[], 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.

API reference

ToggleGroup.Root

Root takes the common props below plus the single- or multiple-mode props selected by type.

PropTypeDescription
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.
valuestring | string[]Controlled selection. string in single mode, string[] (normalized: duplicates and non-strings stripped) in multiple mode. Pair with onValueChange.
defaultValuestring | string[]Initial selection for uncontrolled usage. string in single mode; string[] (default []) in multiple mode.
onValueChange(value: string | undefined) => void | (value: string[]) => voidCalled on every selection change, controlled or not. Receives string | undefined in single mode and the full next string[] in multiple mode.
disabledbooleanDisables every item and short-circuits the group's toggle logic so no value change gets through. Defaults to false.
asChildbooleanRender the single child element as the group container instead of the frame the part renders.
childrenReact.ReactNodeThe toggle items and any layout. Must be a single valid element when asChild is set.

ToggleGroup.Item

PropTypeDescription
valuestringRequired. Identifies this item within the group. Since 0.7.0 it is not rendered as the button's text — pass Text yourself.
disabledbooleanDisables just this item — activation and keyboard toggles are ignored and the button is marked inactive. Defaults to false.
asChildbooleanMerge the toggle behavior onto the single child element instead of the textbutton the part renders. The child must be an activatable button.
childrenReact.ReactNodeRendered inside the item button, or the element to project onto. Must be a single element when asChild is set.