Tabs
tabsSwitches between labeled content panels with underline or solid indicators.
Usage
Basic usage
TabsList installs TabsTab, and each TabsPanel corresponds to value; defaultValue sets the initial selection page.
<Tabs defaultValue="account" className="w-80">
<TabsList>
<TabsTab value="account">Account</TabsTab>
<TabsTab value="password">Password</TabsTab>
</TabsList>
<TabsPanel value="account">Manage your account information and preferences. </TabsPanel>
<TabsPanel value="password">Change the login password here. </TabsPanel>
</Tabs>Segmented Pill Skin
TabsList Add variant="solid" to switch to segmented pill track, and the selected state will slide smoothly by the slider.
<Tabs defaultValue="account" className="w-80">
<TabsList variant="solid">
<TabsTab value="account">Account</TabsTab>
<TabsTab value="password">Password</TabsTab>
<TabsTab value="team">Team</TabsTab>
</TabsList>
<TabsPanel value="account">Account panel. </TabsPanel>
<TabsPanel value="password">Password panel. </TabsPanel>
<TabsPanel value="team">Team Panel. </TabsPanel>
</Tabs>Semantic tone
Adding tone to TabsList gives the selected state a semantic colour: solid keeps a white pill with a semantic label, and the underline indicator follows along. The neutral default keeps the existing neutral selected state, so passing nothing changes nothing.
<Tabs defaultValue="account" className="w-80">
<TabsList variant="solid" tone="brand">
<TabsTab value="account">Account</TabsTab>
<TabsTab value="password">Password</TabsTab>
<TabsTab value="team">Team</TabsTab>
</TabsList>
<TabsPanel value="account">Account panel. </TabsPanel>
<TabsPanel value="password">Password panel. </TabsPanel>
<TabsPanel value="team">Team Panel. </TabsPanel>
</Tabs>Inline switcher (size=sm)
Use sm when the tab bar shares a row with a heading or a search box: 28px track and 24px tab for text only, against 40 / 32 for md. Give the count Tag its own size="sm" as well, or the default md at 24px pushes the tab back up.
<div className="flex items-center gap-2">
<span className="text-sm font-semibold">Reports by title group</span>
<Tabs defaultValue="title">
<TabsList variant="solid" size="sm">
<TabsTab value="title">Title orders<Tag size="sm" className="ml-1.5">2</Tag></TabsTab>
<TabsTab value="paper">Paper orders<Tag size="sm" className="ml-1.5">7</Tag></TabsTab>
</TabsList>
<TabsPanel value="title">Title order list.</TabsPanel>
<TabsPanel value="paper">Paper order list.</TabsPanel>
</Tabs>
</div>Disable a page
Add disabled to TabsTab. This page cannot be clicked or keyboard focused, and other pages can be switched normally.
<Tabs defaultValue="a" className="w-80">
<TabsList>
<TabsTab value="a">Available</TabsTab>
<TabsTab value="b" disabled>Disable</TabsTab>
<TabsTab value="c">Available</TabsTab>
</TabsList>
<TabsPanel value="a">First panel. </TabsPanel>
<TabsPanel value="b">Unreachable. </TabsPanel>
<TabsPanel value="c">Third panel. </TabsPanel>
</Tabs>Vertical arrangement
Tabs Add orientation="vertical", tab lines are arranged vertically, and the direction keys switch up and down.
<Tabs defaultValue="general" orientation="vertical" className="flex w-96 gap-4">
<TabsList className="flex-col items-stretch border-b-0 border-r border-border">
<TabsTab value="general">General</TabsTab>
<TabsTab value="security">Safety</TabsTab>
<TabsTab value="billing">Bill</TabsTab>
</TabsList>
<div className="flex-1">
<TabsPanel value="general">General settings. </TabsPanel>
<TabsPanel value="security">Security settings. </TabsPanel>
<TabsPanel value="billing">Bill settings. </TabsPanel>
</div>
</Tabs>When to use
Use Tabs to switch among peer content panels such as Account, Password, and Team within one region. Panels are mutually exclusive and have no hierarchy. Use Breadcrumb to show a page's place in the site, Anchor for a long-form table of contents that tracks reading progress, or Stepper for an ordered workflow.
Import
import { Tabs, TabsList, TabsTab, TabsPanel, tabsListVariants } from "@hulianui/ui"Props
Tabs forwards Base UI Tabs.Root props including value, defaultValue, onValueChange, and orientation, and is uncontrolled by default. Visual variants belong on TabsList.
Tabs (root)
| Name | Type | Default | Description |
|---|---|---|---|
| value | any | - | Controlled active tab value. |
| defaultValue | any | - | Initially active tab when uncontrolled. |
| orientation | "horizontal" | "vertical" | "horizontal" | Tab-list orientation. |
TabsList
| Name | Type | Default | Description |
|---|---|---|---|
| variant | "underline" | "solid" | "underline" | Underline slider or solid pill styling. |
| size | "sm" | "md" | "md" | Size step, passed down to TabsTab so it never has to be repeated. md is page-level tab navigation; sm is for an inline switcher sitting on the same row as a heading or a search box. See Size. |
| tone | "brand" | "success" | "warning" | "danger" | "neutral" | "neutral" | Semantic colour of the selected state, passed down to TabsTab so it never has to be repeated. See Semantic tone. |
| className | string | - | Additional class name. |
TabsTab accepts required value, plus disabled and className; TabsPanel accepts value and className.
Events
Tabs (root)
| Event | Type | Description |
|---|---|---|
| onValueChange | (value) => void | Called when the active value changes; forwarded to Base UI Tabs.Root. |
Example
<Tabs defaultValue="account" className="w-80">
<TabsList variant="underline">
<TabsTab value="account">Account</TabsTab>
<TabsTab value="password">Password</TabsTab>
<TabsTab value="team" disabled>Team</TabsTab>
</TabsList>
<TabsPanel value="account">Manage your profile and preferences.</TabsPanel>
<TabsPanel value="password">Change your sign-in password.</TabsPanel>
<TabsPanel value="team">Invite members and assign roles.</TabsPanel>
</Tabs>Semantic tone
tone on TabsList describes what being selected means. Its values are a subset of the semantic tone SSOT (see tone on Button): brand, success, warning, danger, and neutral. current is deliberately left out: that step means "set no colour, inherit from the container", and a tab bar does not sit inside a coloured container.
| Variant | Selected label | Slider |
|---|---|---|
solid | Text colour of that tone | Pill keeps bg-surface, so a white pill carries semantic text |
underline | Text colour of that tone | Underline follows the tone |
<TabsList variant="solid" tone="brand">…</TabsList>- The `neutral` default keeps today's rendering, it does not turn the brand colour grey. The selected label stays
text-foregroundand theunderlineindicator staysbg-primary, byte for byte what they were before this prop existed. Existing pages that pass notonedo not move a single pixel; a white pill with a brand-blue label needs an explicittone="brand". - Only the selected state is coloured. Unselected tabs are unaffected and stay
text-muted-foreground, moving totext-foregroundon hover. - The solid pill does not follow `tone`. Filling the pill with a semantic colour would drown out the semantics of the label itself. A soft semantic fill, matching
softon Button, is left for later. Segmentedtakes atonewith the same name and the same values, because it shares this visual language and its selected colour has to match.
Size
md (the default) is sized for page-level tab navigation. A tab bar is often not navigation though, but a switcher on the same row as a heading and a search box, and that row is already 28-32px tall, which md does not fit into:
| Track (solid) | Tab | |
|---|---|---|
md, text only | 40 | 32 |
md, text plus a count Tag | 44 | 36 |
sm, text only | 28 | 24 |
sm, text plus Tag size="sm" | 32 | 28 |
// Inline switcher on the same row as a heading
<div className="flex items-center gap-2">
<span className="text-sm font-semibold">Reports by title group</span>
<Tabs defaultValue="a">
<TabsList variant="solid" size="sm">
<TabsTab value="a">Title orders<Tag size="sm" className="ml-1.5">2</Tag></TabsTab>
<TabsTab value="b">Paper orders<Tag size="sm" className="ml-1.5">7</Tag></TabsTab>
</TabsList>
<TabsPanel value="a">…</TabsPanel>
</Tabs>
</div>A count Tag inside a sm tab needs its own `size="sm"`: Tag defaults to md, which is 24px, and a single one pushes the tab back to 32px. The component does not override a size the child declared explicitly. Reaching in from the outside to restyle an inner component is exactly what product code is told not to do here.
Usage guidelines
- Do not squeeze the height of `TabsList` from product code (
<TabsList className="h-7">). It is aninline-flex items-center, so the tabs merely overflow while staying centred, and the solid pill sticks out 4px above and below the track (measured). Usesize="sm"instead: thepyof the tab and thepof the track have to shrink together, and squeezing only one layer always leaves the pill sticking out.
- [[base-ui-tabs-indicator-slider-via-active-tab-css-vars]]: the slider uses
--active-tab-*variables written by Base UI on the indicator and a CSS transition, with no animation library. The active hook isdata-active, notdata-selected; using the wrong attribute leaves the style inactive. jsdom tests can run without ResizeObserver, although they do not render the indicator's actual geometry.
Related
Breadcrumb · Pagination · Anchor · Affix · BackTop · Stepper
Playground
<Tabs defaultValue="account">
<TabsList variant="underline" size="md">
<TabsTab value="account">Account</TabsTab>
<TabsTab value="password">Password</TabsTab>
</TabsList>
<TabsPanel value="account">…</TabsPanel>
<TabsPanel value="password">…</TabsPanel>
</Tabs>