Tabs
Base UI selection state + roving focus + a moving indicator.
Colors
More tabs than fit — the strip says so on its own
No wrapper at the call site: TabsList carries this itself, because a tab nobody can see is a tab that does not exist.
Variants
Real use · settings panels
Using Tabs
Silica ships one design system through three paths. They share a class vocabulary but are not interchangeable — pick the one that matches what you are building.
Plain HTML — CSS classes
The Tailwind v4 plugin. Works in any framework or none — Rails, Django, PHP, Go, a static file. This path ships no JavaScript, so Tabs is styled but inert: its behavior comes from tabs. For that behavior with no framework, use the node-tree path below.
/* in your CSS */
@import "tailwindcss";
@plugin "@wizeworks/silicaui";Root class tabs, with 16 modifiers:
.tabs.tabs-accent.tabs-boxed.tabs-error.tabs-indicator.tabs-info.tabs-list.tabs-list-scroll.tabs-neutral.tabs-panel.tabs-pills.tabs-primary.tabs-scroller.tabs-secondary.tabs-success.tabs-tab.tabs-warning
React
Typed components over those same classes, with behavior and accessibility from Base UI.
npm i @wizeworks/silicaui-reactimport { Tabs } from "@wizeworks/silicaui-react";Props
TabsProps
| Prop | Type | Required | Description |
|---|---|---|---|
| variant | TabsVariant | no | — |
| color | TabsColor | no | Accent color (underline + pills fill); maps to tabs-<color>. Default primary. |
TabsListProps
| Prop | Type | Required | Description |
|---|---|---|---|
| children | React.ReactNode | no | — |
| indicator | boolean | no | Render the moving underline indicator. Default true. |
| scrollable | boolean | no | Say so when tabs don't fit, instead of letting them fall off the edge. Default true. |
| scrollLabel | string | no | Plural noun naming the scroll controls. Default tabs. |
Source of the demo above
The exact file that renders the examples above, so it can never drift from them. It imports a few of this site’s own layout helpers (../lib/…), so read it for the component calls rather than copying it whole.
Show source
import {
Tabs,
TabsList,
TabsTab,
TabsPanel,
Input,
Button,
} from "@wizeworks/silicaui-react";
import { Section, Row } from "../lib/Section";
import { COLORS } from "../lib/data";
export function TabsDemo() {
return (
<>
<Section title="Colors">
<Row>
{COLORS.slice(0, 6).map((color) => (
<Tabs key={color} color={color} defaultValue="a" className="w-40">
<TabsList>
<TabsTab value="a">One</TabsTab>
<TabsTab value="b">Two</TabsTab>
</TabsList>
</Tabs>
))}
</Row>
</Section>
<Section title="More tabs than fit — the strip says so on its own">
<div className="flex flex-col gap-4">
{(["underline", "boxed", "pills"] as const).map((variant) => (
<div key={variant} className="w-72" data-demo={`overflow-${variant}`}>
<Tabs variant={variant} color="primary" defaultValue="overview">
<TabsList>
{["Overview", "Timeline", "Messages", "Activity", "Documents"].map(
(t) => (
<TabsTab key={t} value={t.toLowerCase()}>
{t}
</TabsTab>
),
)}
</TabsList>
</Tabs>
</div>
))}
<p className="max-w-md text-sm">
No wrapper at the call site: <code>TabsList</code> carries this
itself, because a tab nobody can see is a tab that does not exist.
</p>
</div>
</Section>
<Section title="Variants">
{(["underline", "boxed", "pills"] as const).map((variant) => (
<Tabs key={variant} variant={variant} color="primary" defaultValue="a">
<TabsList>
<TabsTab value="a">Account</TabsTab>
<TabsTab value="b">Password</TabsTab>
<TabsTab value="c">Team</TabsTab>
</TabsList>
</Tabs>
))}
</Section>
<Section title="Real use · settings panels">
<Tabs variant="boxed" color="primary" defaultValue="account" className="max-w-md">
<TabsList>
<TabsTab value="account">Account</TabsTab>
<TabsTab value="password">Password</TabsTab>
<TabsTab value="team">Team</TabsTab>
</TabsList>
<TabsPanel value="account">
<div className="flex flex-col gap-3 pt-4">
<Input placeholder="Full name" defaultValue="Ada Lovelace" />
<Input placeholder="Email" defaultValue="ada@silica.dev" />
<Button color="primary" className="w-fit">
Save
</Button>
</div>
</TabsPanel>
<TabsPanel value="password">
<div className="flex flex-col gap-3 pt-4">
<Input placeholder="Current password" type="password" />
<Input placeholder="New password" type="password" />
<Button color="primary" className="w-fit">
Update password
</Button>
</div>
</TabsPanel>
<TabsPanel value="team">
<p className="pt-4 opacity-70">4 members — 1 seat available.</p>
</TabsPanel>
</Tabs>
</Section>
</>
);
}Vanilla — node tree to HTML
Tabs — tabs; Tab/Panel pair by position. TabsList is a plain wrapper (tabs.ts scopes ownParts to the whole root, not a specific list part). Build a node tree, project it with toHtml(), and let the zero-dependency runtime hydrate it. This is the path for generated or user-authored documents — a site builder, CMS output, a static export.
npm i @wizeworks/silicaui-html @wizeworks/silicaui-behaviorsimport { atom, toHtml } from "@wizeworks/silicaui-html";
const node = atom("Tabs", "tabs", { /* props */ }, [/* children */]);
const markup = toHtml(node);Styling on this path is the class string in the second argument — the same classes as the Plain HTML section above. There is no color prop here; write "tabs tabs-primary" rather than passing a colour as a prop.
- Takes children
- yes
- Hydrated by
tabs
Load @wizeworks/silicaui-behaviors on the page or this markup renders correctly and does nothing.
The full API for Tabs, on every path, is available through the Silica MCP server.