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

Tag

Display-only tag / TagGroup — 5 sizes · neutrals + semantic + brand.

Preview

Live preview
@oshon-ai/components
DesignApprovedAIInternalArchivedBetaHoldNeeds reviewFailed

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 tag

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 { Tag } from '@oshon-ai/components';

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

Colors — 13 Figma variants

Figma "Filled-Tag" component set — 13 colors

Med grayLight grayBlue stonePlumLeafyRosyOliveSkyVioletGravelWhiteWarningError
tsx
<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"

Med grayLight grayBlue stonePlumLeafyRosyOliveSkyVioletGravelWhiteWarningError

size="s"

Med grayLight grayBlue stonePlumLeafyRosyOliveSkyVioletGravelWhiteWarningError

size="m"

Med grayLight grayBlue stonePlumLeafyRosyOliveSkyVioletGravelWhiteWarningError

size="l"

Med grayLight grayBlue stonePlumLeafyRosyOliveSkyVioletGravelWhiteWarningError

size="mobile"

Med grayLight grayBlue stonePlumLeafyRosyOliveSkyVioletGravelWhiteWarningError
tsx
<div style={{ ...col, gap: '1.25rem' }}>
      {SIZES.map((size) => (
        <section key={size} style={col}>
          <h3 style={heading}>size=&quot;{size}&quot;</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.

size="xs"
WarningError
size="s"
WarningError
size="m"
WarningError
size="l"
WarningError
size="mobile"
WarningError
tsx
<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=&quot;{size}&quot;</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

Stateful color → default glyph
AdjustedFailed
Stateful color + custom icon override
PinnedCritical
Stateful color with icon={null} — drops the glyph
No glyphNo glyph
Decorative color + opt-in custom glyph (default behavior is no icon)
FeaturedBeta
tsx
<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

ProductionFrontendv0.9.0A11yNeeds review
InternalDesignDocumentationBlocked
DraftBacklogQ4Archived
tsx
<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.

PropTypeDefaultDescription
children*ReactNodeTag label. Rendered in natural case (Tags do NOT uppercase like Badge).
classNamestringAdditional classes merged after the component's default classes.
colorenumlightGrayColor variant — 13 Figma-authored options. Default `'lightGray'`.
iconReactNodeOptional 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.
sizeenummVisual 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.

tsx
<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.

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

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.

children

The tag label. Rendered in natural case — Tag does NOT uppercase (unlike Badge).

icon

Optional 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

Taxonomy row (six common colors)
<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>
Stateful with auto-glyph
<Tag color="warning">Needs review</Tag>
<Tag color="error">Failed</Tag>
Mobile size (touch)
<Tag color="blueStone" size="mobile">Design</Tag>
Override stateful glyph
<Tag color="warning" icon={<CustomTriangle />}>Needs review</Tag>

✗ Don't

Making Tag interactive
<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).

Hardcoded color override
<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.

Stuffing long sentences into a tag
<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.