Preview
Installation
Install the runtime packages:
pnpm add @oshon-ai/components @oshon-ai/tokens @oshon-ai/primitives
Or scaffold the component source directly into your codebase (shadcn-style):
pnpm dlx @oshon-ai/cli add tab
Wire the tokens into your Tailwind v4 stylesheet:
/* 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.
'use client';
import { Tab } from '@oshon-ai/components';
export default function Example() {
return <Tab />;
}Basic — pill rail (Figma 8332:155286)
primary-100 background + primary-800 text.<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)
primary-700 background with white text. Default chip paints neutral-300.<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
<ControlledDemo />
Vertical — Settings sub-nav (Figma 8986:45619)
<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
neutral-400 text and dims the counter chip to neutral-100 / neutral-500.<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
size=s
size=m
size=l
size=mobile
<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.
<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.
| Attribute | Values | Description |
|---|---|---|
data-oshon-size | xs · s · m · l · mobile | Visual size axis. Mirrors the `size` prop. |
data-oshon-tier | primary · secondary · tertiary | Visual emphasis tier (Button family). Mirrors the `tier` prop. |
data-oshon-state | enabled · active · error · disabled | Component surface state. Set automatically based on props. |
data-disabled | true · (omitted) | Set when `disabled` is true. Pair with `:disabled` CSS for native input components. |
data-state | open · closed · checked · unchecked · … | Radix-derived state for overlay components (Dialog, Tabs, Toggle, etc.). |
/* 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 thedisabledprop