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 tag
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 { Tag } from '@oshon-ai/components';
export default function Example() {
return <Tag />;
}Colors — 13 Figma variants
Figma "Filled-Tag" component set — 13 colors
<div style={{ ...col, maxWidth: 720 }}>
<h3 style={heading}>Figma "Filled-Tag" component set — 13 colors</h3>
<div style={row}>
{COLORS.map((color) => (
<Tag key={color} color={color}>
{labelFor(color)}
</Tag>
))}
</div>
</div>Size matrix — QA grid
size="xs"
size="s"
size="m"
size="l"
size="mobile"
<div style={{ ...col, gap: '1.25rem' }}>
{SIZES.map((size) => (
<section key={size} style={col}>
<h3 style={heading}>size="{size}"</h3>
<div style={row}>
{COLORS.map((color) => (
<Tag key={color} color={color} size={size}>
{labelFor(color)}
</Tag>
))}
</div>
</section>
))}
</div>Stateful — warning + error glyphs
Warning + error are the only Figma-authored stateful colors. Both render a 12×12 leading glyph and apply mix-blend-mode: multiply to the container so the tinted body composites with whatever surface is behind it.
<div style={{ ...col, gap: '1rem' }}>
<h3 style={heading}>
Warning + error are the only Figma-authored stateful colors. Both
render a 12×12 leading glyph and apply{' '}
<code>mix-blend-mode: multiply</code> to the container so the tinted
body composites with whatever surface is behind it.
</h3>
{SIZES.map((size) => (
<section key={size} style={col}>
<div style={caption}>size="{size}"</div>
<div style={row}>
<Tag color="warning" size={size}>
Warning
</Tag>
<Tag color="error" size={size}>
Error
</Tag>
</div>
</section>
))}
</div>Icon overrides — default / custom / dropped
icon={null} — drops the glyph<div style={{ ...col, gap: '1rem' }}>
<section style={col}>
<div style={caption}>Stateful color → default glyph</div>
<div style={row}>
<Tag color="warning">Adjusted</Tag>
<Tag color="error">Failed</Tag>
</div>
</section>
<section style={col}>
<div style={caption}>Stateful color + custom icon override</div>
<div style={row}>
<Tag color="warning" icon={<StarIcon />}>
Pinned
</Tag>
<Tag color="error" icon={<StarIcon />}>
Critical
</Tag>
</div>
</section>
<section style={col}>
<div style={caption}>
Stateful color with <code>icon={'{null}'}</code> — drops the glyph
</div>
<div style={row}>
<Tag color="warning" icon={null}>
No glyph
</Tag>
<Tag color="error" icon={null}>
No glyph
</Tag>
</div>
</section>
<section style={col}>
<div style={caption}>
Decorative color + opt-in custom glyph (default behavior is no icon)
</div>
<div style={row}>
<Tag color="leafy" icon={<StarIcon />}>
Featured
</Tag>
<Tag color="violet" icon={<StarIcon />}>
Beta
</Tag>
</div>
</section>
</div>Taxonomy strip — real-world rail
Mixed Tag rail — typical for a list-row "categories" cluster
<div style={{ ...col, gap: '0.75rem', maxWidth: 640 }}>
<h3 style={heading}>
Mixed Tag rail — typical for a list-row "categories" cluster
</h3>
<div style={row}>
<Tag color="leafy">Production</Tag>
<Tag color="blueStone">Frontend</Tag>
<Tag color="violet">v0.9.0</Tag>
<Tag color="gravel">A11y</Tag>
<Tag color="warning">Needs review</Tag>
</div>
<div style={row}>
<Tag color="medGray">Internal</Tag>
<Tag color="rosy">Design</Tag>
<Tag color="sky">Documentation</Tag>
<Tag color="error">Blocked</Tag>
</div>
<div style={row}>
<Tag color="lightGray">Draft</Tag>
<Tag color="olive">Backlog</Tag>
<Tag color="plum">Q4</Tag>
<Tag color="white">Archived</Tag>
</div>
</div>API
Every prop is documented here directly from the component's TypeScript interface. Inherited DOM attributes (aria-*, onClick, style, etc.) work as usual but are omitted from this table.
| Prop | Type | Default | Description |
|---|---|---|---|
children* | ReactNode | — | Tag label. Rendered in natural case (Tags do NOT uppercase like Badge). |
className | string | — | Additional classes merged after the component's default classes. |
color | enum | lightGray | Color variant — 13 Figma-authored options. Default `'lightGray'`. |
icon | ReactNode | — | Optional leading glyph (12×12). For `color="warning"` / `color="error"` a default glyph is supplied; pass an explicit element to override, or `null` to drop the glyph entirely on a stateful tag. |
size | enum | m | Visual size. Default `'m'` (Figma desktop anchor). |
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.
<Tag 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
Anatomy
The named regions a consumer composes when rendering this component. Each is documented separately so you can target keyboard nav, ARIA labels, and slot props with precision.
childrenThe tag label. Rendered in natural case — Tag does NOT uppercase (unlike Badge).
iconOptional leading 12×12 glyph. The two stateful colors (warning / error) ship a default glyph; pass an explicit React element to override, or `null` to drop the glyph entirely on a stateful tag.
Keyboard
Non-interactive. Tag receives no focus and no keyboard bindings — it is a visual marker. Pass `aria-label` for screen readers when the visible label needs context (e.g. "category: design system"). When you need a focusable / dismissable / selectable pill, compose Chip instead.
Accessibility
Every Oshon component ships axe-clean. We test in CI on every PR and publish the audit log per component.
- WCAG level
- 2.2 AA
- Screen readers tested
- VoiceOver (macOS), NVDA (Windows)
- Last axe audit
- 2026-04-29
Do / Don't
✓ Do
<Tag color="blueStone">Design</Tag> <Tag color="leafy">Approved</Tag> <Tag color="plum">AI</Tag> <Tag color="violet">Internal</Tag> <Tag color="medGray">Archived</Tag> <Tag color="white">Draft</Tag>
<Tag color="warning">Needs review</Tag> <Tag color="error">Failed</Tag>
<Tag color="blueStone" size="mobile">Design</Tag>
<Tag color="warning" icon={<CustomTriangle />}>Needs review</Tag>✗ Don't
<Tag color="blueStone" onClick={remove}>Design</Tag>Tag is a non-interactive <span> by design. Clickable / dismissable / selectable pills are Chip's contract — Chip ships keyboard handling, focus rings, and the appropriate ARIA semantics. Putting `onClick` on Tag's `<span>` ships an a11y failure (no focus ring, no keyboard activation).
<Tag className="bg-purple-600 text-white">Internal</Tag>
Breaks white-labeling (principle #6). Every Tag color flows through @oshon-ai/tokens via Tailwind utilities so applyTheme() retints in one DOM write. If you need a palette outside the 13 Figma-authored colors, extend `TagColor` in `tag-shared.tsx` and add the token mapping — never reach for a one-off class.
<Tag color="lightGray">This is a long category label that wraps</Tag>
Tag pins to `whitespace-nowrap` and a 16px row rhythm. Long labels overflow horizontally and break the row alignment. Cap labels to 1–3 words; reach for Banner / Snackbar when you need a sentence.
Design rationale
Tag and Badge cover orthogonal axes — Tag is taxonomy / category metadata (free-form, 13 colors), Badge is status (six canonical semantic palettes). Both are RSC-safe leaves so they live inside Server Components without a client boundary. The 13 colors come straight from the Figma "Filled-Tag" set; mapping to Oshon tokens is one-to-one for the cool-palette colors (blueStone → primary, plum → plum, leafy → success, oops → error, tradewinds → warning) and uses inline-hex CSS fallbacks for the four data-vis colors (rosy / olive / sky / violet) until the data-vis tokens land in @oshon-ai/tokens. The two stateful colors (warning / error) auto-render a 12×12 leading glyph + mix-blend-multiply so they read clearly when layered over a tinted panel — matching the Figma authoring exactly.