Tabs
Layered panels, one shown at a time, chosen from a list of tabs.
Line
The default: an underline marks the active tab.
Overview
You have 12 active projects and 3 pending tasks.
Analytics
Page views are up 25% on last month.
Reports
Three reports are ready to download.
Settings
Notifications, members and billing.
Code
<Tabs value="overview">
<TabsList label="Project">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="analytics">Analytics</TabsTrigger>
<TabsTrigger value="reports">Reports</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<TabsContent value="overview">...</TabsContent>
...
</Tabs>Usage
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@elements/ui/tabs";
<Tabs value="account">
<TabsList label="Settings">
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
</TabsList>
<TabsContent value="account">Make changes to your account here.</TabsContent>
<TabsContent value="password">Change your password here.</TabsContent>
</Tabs>Examples
Segmented
variant=segmented: a muted track with the active tab raised. Fields in the other tab keep their values.
Make changes to your account here.
Change your password here.
Code
<Tabs variant="segmented" value="account">
<TabsList label="Account settings">
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
</TabsList>
<TabsContent value="account">...fields...</TabsContent>
<TabsContent value="password">...fields...</TabsContent>
</Tabs>Vertical
orientation=vertical stacks the list beside the panel; Up and Down move.
Your name and email.
Change your password.
Email and push settings.
Code
<Tabs orientation="vertical" value="account">
<TabsList label="Preferences">
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
<TabsTrigger value="notifications">Notifications</TabsTrigger>
</TabsList>
...
</Tabs>Disabled
A disabled tab cannot be chosen and arrows skip it.
Home.
Unreachable.
Settings.
Code
<Tabs value="home">
<TabsList label="Sections">
<TabsTrigger value="home">Home</TabsTrigger>
<TabsTrigger value="disabled" disabled>Disabled</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
...
</Tabs>Icons
An Icon beside the label sizes itself.
The rendered component.
Its source.
Code
<Tabs variant="segmented" value="preview">
<TabsList label="View">
<TabsTrigger value="preview"><Icon name="search" /> Preview</TabsTrigger>
<TabsTrigger value="code"><Icon name="pencil" /> Code</TabsTrigger>
</TabsList>
...
</Tabs>Manual activation
activation=manual: arrows move focus, Enter or Space activates.
One.
Two.
Three.
Code
<Tabs activation="manual" value="one">...</Tabs>Controlled
Bind value to state. Clicks write it back; onValueChange fires only for the user's changes.
Alpha.
Bravo.
Charlie.
Code
<Tabs value={state.tab} onValueChange={(v) => state.last = v}>...</Tabs>
<Button onclick={() => state.tab = "c"}>Show C</Button>Guarded
onValueChange runs before the switch with event details (reason, event, cancel). details.cancel() keeps the tab, as an unsaved-changes guard would, until the guard is off.
Unsaved draft.
The rendered preview.
Code
<Tabs value={state.tab} onValueChange={(value, details) => {
if (state.unsaved && !confirm("Discard your changes?")) details.cancel();
}}>
<TabsList label="Editor">...</TabsList>
...
</Tabs>Right to left
dir=rtl lays the tabs out from the right and flips the arrows.
لديك ١٢ مشروعًا نشطًا.
التحليلات.
التقارير.
Code
<Tabs dir="rtl" value="overview">...</Tabs>No starting value, no loop
With no value the first enabled tab is chosen. loop=false stops the arrows at the ends.
Locked.
First step.
Second step.
Last step.
Code
<Tabs loop={false}>
<TabsList label="Steps">
<TabsTrigger value="first">First</TabsTrigger>
...
</TabsList>
</Tabs>No tab active
value null starts with no tab active and no panel. The first tab holds the tab stop; a click or arrow picks one. A placeholder of the panel's size keeps the page still.
Choose a plan to see what it includes.
One project and community support.
Ten projects and shared billing.
Unlimited projects and single sign-on.
Code
<Tabs value={state.plan}> <!-- null: no tab active -->
<TabsList label="Plans">
<TabsTrigger value="starter">Starter</TabsTrigger>
<TabsTrigger value="team">Team</TabsTrigger>
</TabsList>
<p e:if={state.plan === null}>Choose a plan to see what it includes.</p>
<TabsContent value="starter">...</TabsContent>
<TabsContent value="team">...</TabsContent>
</Tabs>Labelled by a heading
attrs on TabsList and TabsContent set aria-labelledby, an id or a test hook, so a visible heading names the tablist.
Team
Four members.
Two pending invites.
Code
<h4 id="team-heading">Team</h4>
<Tabs value="members">
<TabsList attrs={{ "aria-labelledby": "team-heading" }}>...</TabsList>
<TabsContent value="members" attrs={{ id: "members-panel" }}>...</TabsContent>
</Tabs>Closable
With onTabDelete, Delete on a tab closes it and focus moves to the next tab (APG). The caller removes the tab from its list.
Contents of index.ts.
Contents of app.ts.
Contents of style.css.
Contents of README.md.
Code
<Tabs value={state.file} onTabDelete={(v) => state.files = state.files.filter((f) => f !== v)}>
<TabsList label="Open files">
<TabsTrigger e:for={f of state.files} e:key={(f) => f} value={f}>{f}</TabsTrigger>
</TabsList>
<TabsContent e:for={f of state.files} e:key={(f) => f} value={f}>...</TabsContent>
</Tabs>Indicator
Each list has a .tab-indicator that tracks the active tab through --active-tab-left, -top, -width and -height. Style it and the line slides between tabs.
Overview.
Projects.
Account settings.
Code
<!-- .tab-indicator gets --active-tab-left, -top, -width and -height -->
<Tabs class="tabs-sliding" value="overview">...</Tabs>
.tabs-sliding { --tab-active-line: transparent; }
.tabs-sliding .tab-indicator {
top: auto;
bottom: 0;
height: var(--tab-line-width);
background: var(--accent);
transition: left var(--duration-enter), width var(--duration-enter);
}Animated panels
data-activation-direction says which way the selection moved. The new panel slides in from that side with data-starting-style while the old one leaves with data-ending-style.
Overview
Twelve active projects.
Projects
Three due this week.
Account
Signed in as Ada.
Code
<Tabs class="tabs-animated" value="overview">...</Tabs>
/* panels share one grid cell; data-activation-direction says which way */
.tabs-animated > .tab-panel { grid-row: 2; grid-column: 1; transition: transform, opacity; }
.tabs-animated > .tab-panel[data-starting-style][data-activation-direction="right"] {
opacity: 0;
transform: translateX(var(--space-8));
}Links
href renders a tab as a link: still role=tab with one tab stop and arrow keys, and following it navigates. Panels are optional.
Code
<Tabs value="overview">
<TabsList label="Account">
<TabsTrigger value="overview" href="/account">Overview</TabsTrigger>
<TabsTrigger value="billing" href="/account/billing">Billing</TabsTrigger>
</TabsList>
</Tabs>Nested
Tabs inside a panel run on their own.
Inner A.
Inner B, same value as an outer tab.
Outer B.
Code
<Tabs value="outer-a">
...
<TabsContent value="outer-a">
<Tabs variant="segmented" value="inner-a">...</Tabs>
</TabsContent>
</Tabs>In a form
Inactive panels stay mounted, so their fields submit. A required field left empty in another tab activates that tab on submit, so the browser can point at it.
Code
<form onsubmit={() => save(state)}>
<Tabs value="account">
<TabsList label="Profile">...</TabsList>
<TabsContent value="account"><input name="name"></TabsContent>
<TabsContent value="password"><input name="password" type="password" required></TabsContent>
</Tabs>
<Button type="submit">Save</Button>
</form>