Facetcomponents

Tabs

A list, some triggers and switched panels — plus one prop you have to pass.

Terminal window
npx facet-rbxts add tabs

Copies ui/tabs.tsx, plus lib/utils.ts and lib/text.tsx. Needs @facet-ui/react-variants, @lattice-ui/react-runtime@^0.8.0 and @lattice-ui/react-tabs@^0.8.0.

import { Tabs, TabsContent, TabsList, TabsTrigger } from "../shared/ui/tabs";
<Tabs defaultValue="gear">
<TabsList>
<TabsTrigger value="gear" Text="Gear" />
<TabsTrigger value="stats" Text="Stats" />
</TabsList>
<TabsContent value="gear">…</TabsContent>
<TabsContent value="stats">…</TabsContent>
</Tabs>
Two live triggers and one disabled. The selected trigger takes bg-background; its label takes text-foreground.

The parts

PartRendersClasses
TabsFrameflex-col gap-2 w-full h-fit
TabsListFrameflex-row items-center justify-center gap-1 w-fit h-9 rounded-lg bg-muted p-1
TabsTriggerTextButtonflex-row items-center justify-center h-7 w-fit px-3 rounded-md transition duration-150
TabsContentFrameflex-col gap-2 w-full h-fit

Renders a Frame. Unknown props forward onto it and are type-checked against it, so a prop Frame does not accept is a compile error.

That is Tabs, TabsList and TabsContent. TabsTrigger has no passthrough bag — it renders a button and a label, so there is no single instance for an unknown prop to land on.

Pass defaultValue

That is the sharp edge of mirroring a private context: a fallback living inside the primitive is exactly what the mirror was not told.

Props

Tabs

PropTypeDescription
valuestringControlled value. Pass it with onValueChange.
defaultValuestringUncontrolled starting value. Effectively required — see above.
onValueChange(value: string) => voidFires when a different trigger is selected.
classNameClassNameThreaded into the root recipe's className slot inside the component. A class written at a Vela-compiled call site never reaches it — see Overriding from the call site.

TabsTrigger and TabsContent

TabsTrigger takes a required value, plus disabled, Text, children and className. TabsContent takes a required value, plus children and className.

The selected state lands on two instances

<TabsPrimitive.Trigger
className={tabsVariants.trigger({
className: cn(selected && "bg-background", disabled && "opacity-50", props.className),
})}
>
<TextSlot className={cn(tabsVariants.triggerLabel(), selected && "text-foreground")}>

The surface takes bg-background, the label text-foreground over its resting text-muted-foreground — two classes on two instances, because nothing inherits.

The state classes sit inside the recipe’s slot and ahead of props.className — the one rule, since resolution is last-token-wins.

w-fit on the list, h-9 on the list

The list hugs its triggers horizontally and states a fixed height, which gives the pill its shape. p-1 insets the h-7 triggers by 4px each side inside the h-9 track. Changing one number means changing the other.