Latticecomponents

Toggle Group

Selection primitive that coordinates a set of toggle items in single- or multiple-select mode, owning pressed state and motion while you own the item visuals.

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

Toggle Group is the primitive for a related set of toggles that share selection state: view switches, filter chips, text-alignment pickers, and difficulty selectors. Root owns the selected value and tells each Item whether it is pressed, so your items only render their two visual states.

Reach for Toggle Group when several toggles belong together and should behave as one control — either a single-select group where picking one clears the others, or a multiple-select group where each item toggles independently. For a lone on/off control, Checkbox or Switch is the better fit.

Preview

The component running live in the browser — the same @rbxts/react tree Roblox renders, fully interactive.

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

The smallest useful group: an uncontrolled single-select mode picker seeded with defaultValue. Each default item renders as a textbutton labeled with its value, which makes it easy to wire the group up before styling anything. In single mode, activating the already-pressed item clears the selection — onValueChange then fires with undefined — so a toolbar like this can also 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

Switch type to "multiple" and the value becomes a string[]: each item toggles independently, and the array preserves the order in which items were selected. defaultValue seeds the initial set for uncontrolled usage (it defaults to an empty array), and 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 the group reads or sets the selection. Here a start button reads the picked difficulty and stays inert while the selection is cleared — in single mode the controlled value is string | undefined, so design the surrounding UI around the undefined 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: every item ignores activation, and Root’s toggle logic is short-circuited so nothing can change the value while it is set. On an Item it disables just that option while the rest of the group keeps working. Here the loadout is locked during a match, and one class is additionally locked behind premium.

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 marked inactive (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 when you need a different container class, and on each Item to project the toggle behavior onto your own button. Since 0.7.0 the primitive never writes an item’s colors, so read the pressed state from Root and set BackgroundColor3 and TextColor3 yourself (see How it behaves).

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 no longer accepts a transition as of 0.7.0 — the prop existed only to time the color animation that has been removed. Animate your own colors if you want the change to ease.

How it behaves

Value state

ToggleGroup.Root is controllable. Pass value and onValueChange to control the selection, or omit them and pass defaultValue to let the group own it — onValueChange still fires on every change in uncontrolled usage. Each Item reads its own pressed state from the group through context, so controlled and uncontrolled usage behave identically. Activating an item routes through Root, which updates the shared value.

Single vs multiple

The required type prop fixes the group’s behavior and the shape of its value:

  • type="single"value/defaultValue are string, and onValueChange receives string | undefined. Selecting an item replaces the current value; selecting the already-selected item clears it, so the value becomes undefined.
  • type="multiple"value/defaultValue are string[], and onValueChange receives string[]. Each item toggles independently: a newly selected value is appended to the end, so the array preserves selection order. Incoming values (controlled value, defaultValue, and the array handed to onValueChange) are normalized — duplicate and non-string entries are stripped.

Because the value type and the onValueChange signature are a discriminated union over type, set type as a literal so TypeScript can narrow to the right props.

Activation and selection

ToggleGroup.Item toggles on Activated and on Return/Space via InputBegan, so mouse, touch, keyboard, and gamepad activation all work. It renders an unstyled textbutton with AutoButtonColor off, and does not render value as its text — pass a Text of your own. Items are rendered with Selectable={false} on the button itself — manage gamepad selection at the surrounding layout if you need it.

With asChild, the toggle behavior is merged onto your single child element through the shared Slot: the slot’s Active, Selectable, and motion ref win over the child’s own props, and event handlers compose (both the primitive’s Activated/InputBegan handlers and any you pass on the child run). Use a textbutton or imagebutton so Activated fires.

Group container

Root renders a transparent, borderless frame with no size of its own — it is a context boundary, not a laid-out surface. Put a uilistlayout (or your own positioning) inside it to arrange the items, and pass a Size so it reserves space in a flow layout.

Disabled state

disabled on Root disables the entire group; disabled on an Item disables just that one. A disabled item ignores both activation and keyboard toggles and is marked inactive (Active={false}), but keeps rendering its pressed visuals. Group-level disabled additionally short-circuits Root’s toggle logic, so no value change can get through while it is set — including from an item that missed the flag.

Item colors

An item’s BackgroundColor3 and TextColor3 are entirely yours as of 0.7.0. Each item used to animate between fixed active and inactive palettes with no way to opt out; the primitive now 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 to ease rather than snap.

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.