Lattice components

Tabs

Tablist primitive that owns the active value, registers triggers in order, moves selection with the arrow keys, and reveals panels with presence motion.

@lattice-ui/tabs Stable direction import Tabs depends on core , focus , layer , motion

Tabs is the primitive for switching between mutually exclusive views: settings categories, inventory sections, shop pages, or any place where one panel is visible at a time. It owns the active value, keeps its triggers ordered, handles arrow-key movement between them, and mounts the matching panel with presence motion.

Reach for Tabs when a set of triggers should drive value-based selection — exactly one active at a time — with keyboard and gamepad movement across the list and panels that animate in and out as the value changes.

Import

import { Tabs } from "@lattice-ui/tabs";

Anatomy

Compose Root around a List of Triggers and one Content per value. Every Trigger and Content is tied to the active value through a shared value string.

Tabs anatomy
<Tabs.Root>
<Tabs.List>
<Tabs.Trigger value="..." />
</Tabs.List>
<Tabs.Content value="..." />
</Tabs.Root>
PartRequiredResponsibility
Tabs.RootyesOwns the active value, the trigger registry, and orientation; shares them through context.
Tabs.ListyesA container frame that groups the triggers.
Tabs.TriggeryesA textbutton that selects its value on activation, selection, or Enter/Space.
Tabs.ContentyesA panel that mounts and animates while its value is active.

Example

A controlled tab group with three panels, driven by value/onValueChange, with a disabled trigger.

SettingsTabs.tsx
import { useState } from "@rbxts/react";
import { Tabs } from "@lattice-ui/tabs";
export function SettingsTabs() {
const [tab, setTab] = useState("general");
return (
<Tabs.Root value={tab} onValueChange={setTab} orientation="horizontal">
<Tabs.List>
<uilistlayout FillDirection={Enum.FillDirection.Horizontal} Padding={new UDim(0, 6)} />
<Tabs.Trigger value="general">
<textlabel BackgroundTransparency={1} Text="General" Size={UDim2.fromOffset(120, 34)} />
</Tabs.Trigger>
<Tabs.Trigger value="audio">
<textlabel BackgroundTransparency={1} Text="Audio" Size={UDim2.fromOffset(120, 34)} />
</Tabs.Trigger>
<Tabs.Trigger value="advanced" disabled>
<textlabel BackgroundTransparency={1} Text="Advanced" Size={UDim2.fromOffset(120, 34)} />
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="general">
<frame BackgroundColor3={Color3.fromRGB(24, 26, 32)} Size={UDim2.fromOffset(320, 200)}>
<textlabel BackgroundTransparency={1} Text="General settings" Size={UDim2.fromScale(1, 1)} />
</frame>
</Tabs.Content>
<Tabs.Content value="audio">
<frame BackgroundColor3={Color3.fromRGB(24, 26, 32)} Size={UDim2.fromOffset(320, 200)}>
<textlabel BackgroundTransparency={1} Text="Audio settings" Size={UDim2.fromScale(1, 1)} />
</frame>
</Tabs.Content>
<Tabs.Content value="advanced">
<frame BackgroundColor3={Color3.fromRGB(24, 26, 32)} Size={UDim2.fromOffset(320, 200)}>
<textlabel BackgroundTransparency={1} Text="Advanced settings" Size={UDim2.fromScale(1, 1)} />
</frame>
</Tabs.Content>
</Tabs.Root>
);
}

How it behaves

Value state

Tabs.Root is controllable. Pass value and onValueChange to control it, or defaultValue to run uncontrolled. The active value is a plain string that each Trigger and Content matches against. onValueChange fires only with a defined value, so you never receive an undefined selection.

When the active value points at no enabled trigger — on first mount with no default, or after the selected trigger is removed or disabled — Root resolves a replacement: it falls back to the first enabled trigger, or to the next enabled trigger after the one that was last selected. This keeps a valid panel visible as triggers mount, unmount, or toggle their disabled state.

Selection

Each Tabs.Trigger registers itself with Root in mount order, exposing a stable order used to resolve fallbacks and arrow-key movement. A trigger becomes active in three ways: pointer activation, Roblox SelectionGained (so moving a gamepad cursor onto a trigger selects it), and pressing Enter or Space while focused. Disabled triggers set Active and Selectable to false, are skipped during movement, and never become the active value.

Tabs.Trigger registers with the focus system through useFocusNode, so it participates in gamepad selection alongside other focusable nodes.

Orientation

orientation defaults to "horizontal". It controls which arrow keys move the selection: Left/Right when horizontal, Up/Down when vertical. Pressing a movement key focuses the next enabled trigger in that direction and selects it in one step. Orientation is purely behavioral — it does not lay out the List, so add your own uilistlayout (as in the example) to position the triggers.

Panels and presence

Tabs.Content is tied to a value and is present only while that value is active. By default it mounts through a Presence boundary running a surface-reveal recipe, so the panel animates in when selected and animates out when another value takes over, unmounting after the exit completes. Override the recipe with transition. Pass forceMount to keep the panel mounted at all times and drive visibility yourself — the content stays in the tree and toggles its Visible property based on the active value and motion phase.

API reference

Tabs.Root

Prop Type Description
value string Controlled active value. Pair with onValueChange.
defaultValue string Initial active value for uncontrolled usage. When omitted, the first enabled trigger is selected.
onValueChange (value: string) => void Called whenever the active value changes. Always receives a defined value.
orientation "horizontal" | "vertical" Arrow-key movement axis. Defaults to "horizontal".
children React.ReactNode The List and Content parts.

Tabs.List

Prop Type Description
asChild boolean Merge the list onto the single child element instead of rendering the default frame.
children React.ReactNode The trigger elements. Required as a single element when asChild is set.

Tabs.Trigger

Prop Type Description
value string The value this trigger selects when activated. Required.
asChild boolean Merge selection behavior onto the single child element instead of rendering the default textbutton.
disabled boolean Removes the trigger from selection and movement and prevents it from becoming active. Defaults to false.
children React.ReactElement The element to render. Required when asChild is set.

Tabs.Content

Prop Type Description
value string The value this panel is shown for. Required.
asChild boolean Merge the panel onto the single child element instead of rendering the default frame.
forceMount boolean Keeps the panel mounted at all times and toggles visibility instead of unmounting on exit.
transition PresenceMotionConfig Overrides the default surface-reveal recipe.
children React.ReactNode The panel contents.