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-react
import { Tabs } from "@wizeworks/silicaui-react";

Props

TabsProps
PropTypeRequiredDescription
variantTabsVariantno—
colorTabsColornoAccent color (underline + pills fill); maps to tabs-<color>. Default primary.
TabsListProps
PropTypeRequiredDescription
childrenReact.ReactNodeno—
indicatorbooleannoRender the moving underline indicator. Default true.
scrollablebooleannoSay so when tabs don't fit, instead of letting them fall off the edge. Default true.
scrollLabelstringnoPlural 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-behaviors
import { 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.