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

InputChip

Dismissible input/filter chip.

Preview

Live preview
@oshon-ai/components
Design
Engineering
Marketing

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 inputchip

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

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

Removable tags

    design-system
    figma
    tailwind-v4
    storybook
Remaining: 4
tsx
<InputChipPlayground />

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*ReactNodeChip label.
classNamestringAdditional classes merged after the component's default classes.
dismissLabelstringAccessible label for the dismiss button. Required when `onDismiss` is provided so screen readers announce it ("Remove {label}").
dismissPermissionsPartial<PermissionContext>Permission props forwarded to the nested dismiss HeadlessButton. The dismiss is the only interactive surface on this chip, so this is how consumers gate removability.
leadingIconReactNodeOptional leading icon rendered before the label. Typically the exported `DragIcon` from `chip-shared.tsx` for draggable-tag affordances, but any ReactNode is accepted for one-off leading glyphs (country flag, avatar, etc).
onDismiss((event: MouseEvent<HTMLButtonElement, MouseEvent>) => void)Fired when the user activates the dismiss affordance. When omitted, the dismiss slot is hidden (the chip becomes a static tag).
sizeenummVisual size. Default `'m'` — maps to Figma "Size=desktop".

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

leadingIcon

Icon rendered before the label. Choice / Selection swap it for a check icon when selected. Pivot / Input accept it as-is. CounterChip has no leading slot.

children

Chip label (or count for CounterChip). Wrapped in a <span data-oshon-slot='label'> for interactive variants.

trailingIcon

Pivot only. Optional icon rendered after the label (for e.g. a dropdown affordance on a pivot that opens a subfilter).

dismiss

Input only. Rendered when `onDismiss` is provided. Composes @oshon-ai/primitives/button so the dismiss carries its own permission + audit plumbing independent of any surrounding context.

Keyboard

Enter / Space: activate (Choice / Selection / Pivot). Tab: focus. Shift+Tab: focus backward. Choice: role="radio", aria-checked reflects `selected`. Selection: role="checkbox", aria-checked reflects `checked`. Pivot: aria-pressed reflects `pressed`. Input: non-button root; the dismiss button is keyboard-reachable via Tab and activates with Enter / Space. Counter: non-interactive — receives no focus.

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-20

Do / Don't

✓ Do

Single-select (Choice)
<div role="radiogroup" aria-label="Plan">
  <Chip.Choice selected={plan === 'free'} onClick={() => setPlan('free')}>Free</Chip.Choice>
  <Chip.Choice selected={plan === 'pro'} onClick={() => setPlan('pro')}>Pro</Chip.Choice>
</div>
Multi-select (Selection)
<Chip.Selection checked={tags.has('bug')} onClick={() => toggle('bug')}>Bug</Chip.Selection>
Pivot toggle
<Chip.Pivot pressed={active === 'open'} onClick={() => setActive('open')}>Open</Chip.Pivot>
Removable tag (Input)
<Chip.Input onDismiss={() => remove(tag)} dismissLabel={`Remove ${tag}`}>{tag}</Chip.Input>

✗ Don't

Nesting a button inside a chip button
<Chip.Pivot>
  <button onClick={dismiss}>×</button>
</Chip.Pivot>

Nested interactive elements are invalid HTML and fail axe (WCAG 2.2 4.1.2). Use Chip.Input — its root is a <div> so the nested dismiss can itself be a real button.

Hardcoded color override
<Chip.Choice className="bg-blue-600">Free</Chip.Choice>

Breaks white-labeling. All colors must flow through @oshon-ai/tokens. If you need a different surface, add a variant to the manifest and compose it via tokens. See OSHON design principle #6.

CounterChip with an onClick
<Chip.Counter onClick={openDetails}>12</Chip.Counter>

CounterChip is a non-interactive leaf. If you need a clickable count, wrap it in a Button or compose it inside a Chip.Pivot — keep the count leaf-only so RSC benefits stick.

Choice without a radiogroup wrapper
<Chip.Choice selected={a} onClick={pickA}>A</Chip.Choice>
<Chip.Choice selected={b} onClick={pickB}>B</Chip.Choice>

Choice chips emit role="radio" — a screen reader expects a role="radiogroup" ancestor to announce group context ("1 of 3"). Wrap sibling Chip.Choice elements in <div role="radiogroup" aria-label="…">.

Design rationale

Chip fan-out follows ADR-002 (file-per-variant) for pin-point tree shaking; `chip-shared.tsx` holds the single source of truth for size scale, surface palettes, and icons so each variant file differs only in role semantics + slot contract. One family manifest (not five) matches the consumer mental model ("it's all Chip") and avoids 90%-duplicated manifest documents. The Choice/Selection visual similarity is intentional — the distinction is semantic, announced by assistive tech; visual convergence plus semantic divergence is a long-standing DS 3.1 pattern. CounterChip is the first leaf in @oshon-ai/components that doesn't need a client boundary; proving principle #5 here unlocks Badge / Avatar / Divider / Typography landing as RSC leaves later.