UNPKG

@loke/ui

Version:
197 lines (148 loc) 6.37 kB
--- name: tabs type: core domain: navigation requires: [loke-ui] description: > Tabbed interfaces with Tabs/TabsList/TabsTrigger/TabsContent composition. Value-based trigger-to-content linking via matching value props. Roving focus via RovingFocusGroup. activationMode automatic (focus selects) vs manual (Enter/Space selects). forceMount on TabsContent for exit animations via Presence. orientation horizontal (default) or vertical. --- # Tabs ## Setup Basic tabs with a list, triggers, and content panels. ```tsx import { Tabs, TabsList, TabsTrigger, TabsContent, } from "@loke/ui/tabs"; function ProfileTabs() { return ( <Tabs defaultValue="account"> <TabsList> <TabsTrigger value="account">Account</TabsTrigger> <TabsTrigger value="password">Password</TabsTrigger> <TabsTrigger value="notifications">Notifications</TabsTrigger> </TabsList> <TabsContent value="account"> <p>Manage your account settings.</p> </TabsContent> <TabsContent value="password"> <p>Change your password here.</p> </TabsContent> <TabsContent value="notifications"> <p>Configure notification preferences.</p> </TabsContent> </Tabs> ); } ``` ## Core Patterns ### Manual activation By default (`activationMode="automatic"`), focusing a tab via arrow keys immediately selects it and shows its panel. In manual mode, focus and selection are separate — the user must press Enter or Space to activate the focused tab. Use manual mode when switching tabs triggers expensive operations (network requests, heavy rendering). ```tsx <Tabs defaultValue="overview" activationMode="manual"> <TabsList> <TabsTrigger value="overview">Overview</TabsTrigger> <TabsTrigger value="analytics">Analytics</TabsTrigger> <TabsTrigger value="reports">Reports</TabsTrigger> </TabsList> <TabsContent value="overview">Overview panel</TabsContent> <TabsContent value="analytics">Analytics panel</TabsContent> <TabsContent value="reports">Reports panel</TabsContent> </Tabs> ``` Source: `src/components/tabs/tabs.tsx` — `activationMode !== "manual"` check in `onFocus`. ### Vertical tabs Sets `orientation="vertical"` so arrow navigation uses ArrowUp/ArrowDown instead of ArrowLeft/ArrowRight. ```tsx <Tabs defaultValue="general" orientation="vertical"> <TabsList> <TabsTrigger value="general">General</TabsTrigger> <TabsTrigger value="security">Security</TabsTrigger> <TabsTrigger value="billing">Billing</TabsTrigger> </TabsList> <TabsContent value="general">General settings</TabsContent> <TabsContent value="security">Security settings</TabsContent> <TabsContent value="billing">Billing settings</TabsContent> </Tabs> ``` Style with CSS using `data-orientation="vertical"` on the root `Tabs` element. ### Animated panels with `forceMount` Without `forceMount`, `TabsContent` unmounts immediately when another tab is selected, preventing CSS exit animations. With `forceMount`, the panel stays in the DOM and `data-state` toggles between `"active"` and `"inactive"` — CSS animations can target these. ```tsx <Tabs defaultValue="a"> <TabsList> <TabsTrigger value="a">Tab A</TabsTrigger> <TabsTrigger value="b">Tab B</TabsTrigger> </TabsList> <TabsContent value="a" forceMount> Panel A </TabsContent> <TabsContent value="b" forceMount> Panel B </TabsContent> </Tabs> ``` ```css /* TabsContent starts hidden via hidden attribute when inactive */ [role="tabpanel"][data-state="active"] { animation: tab-in 150ms ease-out; } @keyframes tab-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } } ``` Note: `TabsContent` sets `hidden` on the DOM element when inactive (even with `forceMount`) and only renders `children` when `present`. The `animationDuration` is forced to `"0s"` on initial mount to prevent the active tab from animating in on first render. Source: `src/components/tabs/tabs.tsx` — `Presence` + `isMountAnimationPreventedRef`. ### Controlled state ```tsx const [tab, setTab] = useState("account"); <Tabs value={tab} onValueChange={setTab}> <TabsList> <TabsTrigger value="account">Account</TabsTrigger> <TabsTrigger value="password">Password</TabsTrigger> </TabsList> <TabsContent value="account">Account panel</TabsContent> <TabsContent value="password">Password panel</TabsContent> </Tabs> ``` ## Common Mistakes ### Mismatched `value` props between trigger and content — panel never shows `TabsTrigger` and `TabsContent` are linked by an exact string match on `value`. A mismatch means the content panel is never marked active. ```tsx // Wrong — values don't match <TabsTrigger value="acct">Account</TabsTrigger> <TabsContent value="account">...</TabsContent> // Correct <TabsTrigger value="account">Account</TabsTrigger> <TabsContent value="account">...</TabsContent> ``` Source: `src/components/tabs/tabs.tsx` — `makeTriggerId`/`makeContentId` generate IDs from value; `isSelected = value === context.value`. ### Not understanding automatic vs manual activation — unexpected UX on keyboard nav In `automatic` mode (the default), pressing ArrowRight focuses the next tab **and immediately selects it**, showing its panel. Users accustomed to arrow-then-Enter workflows will trigger panels unintentionally. Use `activationMode="manual"` when tab selection has side effects. Source: `src/components/tabs/tabs.tsx` — `onFocus` fires `onValueChange` when `activationMode !== "manual"`. ### `TabsContent` outside `Tabs` root — context error at runtime `TabsContent` must be a descendant of `Tabs`. It reads context for `value`, `baseId`, and `orientation`. Rendering it outside throws a context missing error. ```tsx // Wrong <Tabs defaultValue="a"> <TabsList>...</TabsList> </Tabs> <TabsContent value="a">...</TabsContent> {/* outside Tabs — throws */} // Correct <Tabs defaultValue="a"> <TabsList>...</TabsList> <TabsContent value="a">...</TabsContent> </Tabs> ``` Source: `src/components/tabs/tabs.tsx` — `useTabsContext` throws if no provider found. ## Cross-references - **Accordion** (`@loke/ui/accordion`) — for expandable sections where multiple panels can be visible simultaneously - **Choosing the Right Component** — Tabs vs Accordion decision guidance