Welcome to Oshon · v1.0  ·  Now in public beta for enterprise teams Read the launch notes
NavigationUpdatedFree

Tab

Tab / TabList.

Preview

Live preview
@oshon-ai/components
Dashboard widgets and high-level metrics live here.

Installation

Install the runtime packages:

pnpm
pnpm add @oshon-ai/components @oshon-ai/tokens @oshon-ai/primitives

Or scaffold the component source directly into your codebase (shadcn-style):

pnpm
pnpm dlx @oshon-ai/cli add tab

Wire the tokens into your Tailwind v4 stylesheet:

css
/* app/globals.css */
@import 'tailwindcss';
@import '@oshon-ai/tokens/css';
@import '@oshon-ai/tokens/tailwind';

New here? Walk through the full setup — prereqs, theming, your first render.

Usage

Import the component and render it. Every component supports the standard tier, size, and disabled props where applicable.

tsx
'use client';
import { Tab } from '@oshon-ai/components';

export default function Example() {
  return <Tab />;
}

Basic — pill rail (Figma 8332:155286)

Overview panel content goes here.
Default pill at desktop size (m=36px). Click a tab — selected paintsprimary-100 background + primary-800 text.
tsx
<div style={card}>
      <Tabs.Root defaultValue="overview">
        <Tabs.List aria-label="Sections">
          <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
          <Tabs.Trigger value="activity">Activity</Tabs.Trigger>
          <Tabs.Trigger value="settings">Settings</Tabs.Trigger>
        </Tabs.List>
        <Tabs.Content value="overview">
          <div style={panelText}>Overview panel content goes here.</div>
        </Tabs.Content>
        <Tabs.Content value="activity">
          <div style={panelText}>Activity panel content goes here.</div>
        </Tabs.Content>
        <Tabs.Content value="settings">
          <div style={panelText}>Settings panel content goes here.</div>
        </Tabs.Content>
      </Tabs.Root>
      <div style={caption}>
        Default pill at desktop size (m=36px). Click a tab — selected paints
        <code>primary-100</code> background + <code>primary-800</code> text.
      </div>
    </div>

With counter — chip flips on selection (Figma 2865:9979)

12 unread messages.
Selected counter chip paints solid primary-700 background with white text. Default chip paints neutral-300.
tsx
<div style={card}>
      <Tabs.Root defaultValue="inbox">
        <Tabs.List aria-label="Mail folders">
          <Tabs.Trigger value="inbox" counter={12}>
            Inbox
          </Tabs.Trigger>
          <Tabs.Trigger value="drafts" counter={3}>
            Drafts
          </Tabs.Trigger>
          <Tabs.Trigger value="sent" counter={128}>
            Sent
          </Tabs.Trigger>
          <Tabs.Trigger value="archived" counter={0}>
            Archived
          </Tabs.Trigger>
        </Tabs.List>
        <Tabs.Content value="inbox">
          <div style={panelText}>12 unread messages.</div>
        </Tabs.Content>
        <Tabs.Content value="drafts">
          <div style={panelText}>3 drafts saved.</div>
        </Tabs.Content>
        <Tabs.Content value="sent">
          <div style={panelText}>128 messages sent this week.</div>
        </Tabs.Content>
        <Tabs.Content value="archived">
          <div style={panelText}>Archive is empty.</div>
        </Tabs.Content>
      </Tabs.Root>
      <div style={caption}>
        Selected counter chip paints solid <code>primary-700</code> background
        with white text. Default chip paints <code>neutral-300</code>.
      </div>
    </div>

Controlled — value + onValueChange

Plan settings…
Active tab tracked in parent: plan. Use this when the active tab needs to be persisted (URL, query param) or driven by an external action.
tsx
<ControlledDemo />

Vertical — Settings sub-nav (Figma 8986:45619)

Profile fields…
tsx
<div style={card}>
      <Tabs.Root orientation="vertical" defaultValue="profile">
        <Tabs.List aria-label="Account settings">
          <Tabs.Trigger size="l" value="profile">
            Profile
          </Tabs.Trigger>
          <Tabs.Trigger size="l" value="notifications" counter={5}>
            Notifications
          </Tabs.Trigger>
          <Tabs.Trigger size="l" value="security">
            Security
          </Tabs.Trigger>
          <Tabs.Trigger size="l" value="api-keys" counter={2}>
            API keys
          </Tabs.Trigger>
        </Tabs.List>
        <Tabs.Content value="profile">
          <div style={panelText}>Profile fields…</div>
        </Tabs.Content>
        <Tabs.Content value="notifications">
          <div style={panelText}>5 notification rules…</div>
        </Tabs.Content>
        <Tabs.Content value="security">
          <div style={panelText}>Two-factor + sessions…</div>
        </Tabs.Content>
        <Tabs.Content value="api-keys">
          <div style={panelText}>2 active API keys…</div>
        </Tabs.Content>
      </Tabs.Root>
    </div>

Disabled trigger — Figma 1162:21882

Active workflows…
Disabled trigger paints neutral-400 text and dims the counter chip to neutral-100 / neutral-500.
tsx
<div style={card}>
      <Tabs.Root defaultValue="active">
        <Tabs.List aria-label="Workflow stages">
          <Tabs.Trigger value="active">Active</Tabs.Trigger>
          <Tabs.Trigger value="paused">Paused</Tabs.Trigger>
          <Tabs.Trigger value="archived" disabled counter={0}>
            Archived
          </Tabs.Trigger>
        </Tabs.List>
        <Tabs.Content value="active">
          <div style={panelText}>Active workflows…</div>
        </Tabs.Content>
        <Tabs.Content value="paused">
          <div style={panelText}>Paused workflows…</div>
        </Tabs.Content>
        <Tabs.Content value="archived">
          <div style={panelText}>Archived workflows…</div>
        </Tabs.Content>
      </Tabs.Root>
      <div style={caption}>
        Disabled trigger paints <code>neutral-400</code> text and dims the
        counter chip to <code>neutral-100 / neutral-500</code>.
      </div>
    </div>

Size matrix (playground) — xs / s / m / l / mobile

size=xs

Overview at xs.

size=s

Overview at s.

size=m

Overview at m.

size=l

Overview at l.

size=mobile

Overview at mobile.
tsx
<div style={{ ...col, gap: '1.5rem' }}>
      {SIZES.map((size) => (
        <section key={size} style={card}>
          <h3 style={heading}>size={size}</h3>
          <Tabs.Root defaultValue="overview">
            <Tabs.List aria-label={`Sections size ${size}`}>
              <Tabs.Trigger size={size} value="overview">
                Overview
              </Tabs.Trigger>
              <Tabs.Trigger size={size} value="activity" counter={4}>
                Activity
              </Tabs.Trigger>
              <Tabs.Trigger size={size} value="settings">
                Settings
              </Tabs.Trigger>
            </Tabs.List>
            <Tabs.Content value="overview">
              <div style={panelText}>Overview at {size}.</div>
            </Tabs.Content>
            <Tabs.Content value="activity">
              <div style={panelText}>Activity at {size}.</div>
            </Tabs.Content>
            <Tabs.Content value="settings">
              <div style={panelText}>Settings at {size}.</div>
            </Tabs.Content>
          </Tabs.Root>
        </section>
      ))}
    </div>

Styling

Three layers of customization, in order of escape-hatch strength: className overrides → data-attribute targeting → CSS custom properties.

Passing Tailwind classes

Every Oshon component accepts a className prop merged AFTER the component's default classes. Use it to override spacing, color, or size without forking the component.

tsx
<Tab
  className="ring-2 ring-offset-2 ring-blue-500"
/>

Data attributes

Oshon components expose their internal state as data-oshon-* attributes so you can target them from CSS without coupling to internal class names. The most common attributes are listed below — see the component's source for the full set.

AttributeValuesDescription
data-oshon-sizexs · s · m · l · mobileVisual size axis. Mirrors the `size` prop.
data-oshon-tierprimary · secondary · tertiaryVisual emphasis tier (Button family). Mirrors the `tier` prop.
data-oshon-stateenabled · active · error · disabledComponent surface state. Set automatically based on props.
data-disabledtrue · (omitted)Set when `disabled` is true. Pair with `:disabled` CSS for native input components.
data-stateopen · closed · checked · unchecked · …Radix-derived state for overlay components (Dialog, Tabs, Toggle, etc.).
css
/* Target the secondary tier specifically */
[data-oshon-tier="secondary"] {
  --oshon-color-primary-700: var(--my-brand-color);
}

Interactive states

Every interactive component supports the standard CSS pseudo- classes plus Tailwind's state variants. Focus rings always use :focus-visible so keyboard users see them but mouse users don't.

  • :hover / hover:* — pointer hover
  • :focus-visible / focus-visible:* — keyboard focus
  • :active / active:* — pressed
  • :disabled / disabled:* — set via the disabled prop