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 select
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 { Select } from '@oshon-ai/components';
export default function Example() {
return <Select />;
}Default
<Select.Root>
<Select.Trigger size="m" aria-label="Framework">
<Select.Value placeholder="Select…" />
</Select.Trigger>
<Select.Content>
<Select.Item value="react">React</Select.Item>
<Select.Item value="vue">Vue</Select.Item>
<Select.Item value="svelte">Svelte</Select.Item>
</Select.Content>
</Select.Root>Size matrix
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', alignItems: 'flex-start' }}>
<div key="xs" data-story-size="xs">
<Select.Root>
<Select.Trigger size="xs" aria-label="Framework">
<Select.Value placeholder="Select…" />
</Select.Trigger>
<Select.Content>
<Select.Item value="react">React</Select.Item>
<Select.Item value="vue">Vue</Select.Item>
<Select.Item value="svelte">Svelte</Select.Item>
</Select.Content>
</Select.Root>
</div>
<div key="s" data-story-size="s">
<Select.Root>
<Select.Trigger size="s" aria-label="Framework">
<Select.Value placeholder="Select…" />
</Select.Trigger>
<Select.Content>
<Select.Item value="react">React</Select.Item>
<Select.Item value="vue">Vue</Select.Item>
<Select.Item value="svelte">Svelte</Select.Item>
</Select.Content>
</Select.Root>
</div>
<div key="m" data-story-size="m">
<Select.Root>
<Select.Trigger size="m" aria-label="Framework">
<Select.Value placeholder="Select…" />
</Select.Trigger>
<Select.Content>
<Select.Item value="react">React</Select.Item>
<Select.Item value="vue">Vue</Select.Item>
<Select.Item value="svelte">Svelte</Select.Item>
</Select.Content>
</Select.Root>
</div>
<div key="l" data-story-size="l">
<Select.Root>
<Select.Trigger size="l" aria-label="Framework">
<Select.Value placeholder="Select…" />
</Select.Trigger>
<Select.Content>
<Select.Item value="react">React</Select.Item>
<Select.Item value="vue">Vue</Select.Item>
<Select.Item value="svelte">Svelte</Select.Item>
</Select.Content>
</Select.Root>
</div>
<div key="mobile" data-story-size="mobile">
<Select.Root>
<Select.Trigger size="mobile" aria-label="Framework">
<Select.Value placeholder="Select…" />
</Select.Trigger>
<Select.Content>
<Select.Item value="react">React</Select.Item>
<Select.Item value="vue">Vue</Select.Item>
<Select.Item value="svelte">Svelte</Select.Item>
</Select.Content>
</Select.Root>
</div>
</div>Permission denied
<Select.Root permissions={{ can: () => false }}>
<Select.Trigger size="m" aria-label="Framework">
<Select.Value placeholder="Select…" />
</Select.Trigger>
<Select.Content>
<Select.Item value="react">React</Select.Item>
<Select.Item value="vue">Vue</Select.Item>
<Select.Item value="svelte">Svelte</Select.Item>
</Select.Content>
</Select.Root>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.
<Select 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.
TriggerThe form-field surface. Takes `size="xs|s|m|l|mobile"` and displays either Value (selected) or the placeholder (unset).
ValuePass-through of the primitive Value; Radix chooses between selected text and `[data-placeholder]` span styled via the Trigger.
IconTrailing chevron/indicator. Rotates 180° when the listbox is open via `data-state="open"`.
ContentListbox surface rendered through a portal. Width matches the Trigger (`--radix-select-trigger-width`); height caps at the available viewport space.
ItemSingle selectable row. Highlight state driven by `data-highlighted`; disabled state by `data-disabled`.
ItemIndicatorCheckmark/indicator slot shown only on the selected Item.
SeparatorThin divider between Item groups.
LabelCaption heading above an item group; uppercase small-caps treatment.
Keyboard
Space/Enter: open listbox. Up/Down: move highlight (typeahead supported). Enter: commit. Esc: close without committing. Tab: close + move focus. (Behavior inherited from @radix-ui/react-select via @oshon-ai/primitives/select.)
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
<Select.Root defaultValue="open">
<Select.Trigger aria-label="Status">
<Select.Value placeholder="Select…" />
<Select.Icon>▾</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content>
<Select.Viewport>
<Select.Item value="open">
<Select.ItemText>Open</Select.ItemText>
<Select.ItemIndicator>✓</Select.ItemIndicator>
</Select.Item>
<Select.Item value="closed">
<Select.ItemText>Closed</Select.ItemText>
<Select.ItemIndicator>✓</Select.ItemIndicator>
</Select.Item>
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root><Select.Viewport>
<Select.Group>
<Select.Label>Active</Select.Label>
<Select.Item value="open">…</Select.Item>
<Select.Item value="in-review">…</Select.Item>
</Select.Group>
<Select.Separator />
<Select.Group>
<Select.Label>Archived</Select.Label>
<Select.Item value="closed">…</Select.Item>
</Select.Group>
</Select.Viewport><Select.Trigger size="l">…</Select.Trigger>
✗ Don't
<Select.Viewport>{thousandsOfItems.map(...)}</Select.Viewport>Select is a listbox pattern sized for small enumerations. For density regimes beyond `card`/`standard` (see ARCHITECTURE §2) use the Combobox / search-backed picker in @oshon-ai/data (Phase 5).
<Select.Root><Select.Content>…</Select.Content></Select.Root>
Content must live inside Portal so the listbox escapes any ancestor with `overflow: hidden` or a lower stacking context; otherwise it clips and breaks collision detection.
Design rationale
Phase 4a anchor for list-based form fields. Closes the flagship trio (Button + Dialog + Select). Proves the styled-compound pattern for Radix-backed primitives with many sub-components, and pins the listbox surface styling (radius, shadow, z-index, trigger-width binding) that Combobox + Menu + Popover-picker will inherit in 4b+. See ADR-001, ADR-002, ADR-003.