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 menuitem
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 { MenuItem } from '@oshon-ai/components';
export default function Example() {
return <MenuItem />;
}Interactive — all features, 3 tiers
- Grouped sections (Actions, Views, Status)
- Leading icons on each item
- Keyboard-hint keycaps (⌘E, ⌘⇧S, ⌫)
- Inline toggles (Compact view, Show archived) — flipping does not close the menu
- Single-select "selected" state inside Status
- Multi-line items (Pricing rule) with their own submenus
- Three-tier cascade (Share → Assign to → searchable teammate list)
- Disabled item (Delete)
onAction and closes the menu.<InteractivePlayground />
Auto-search density (≥13 items)
Override the cutoff with
autoSearchThreshold, or force the field on/off with searchable.<AutoSearchPlayground />
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 |
|---|---|---|---|
items* | MenuEntry[] | — | Items to render (mix of item, divider, and group entries). |
aria-label | string | — | Accessible label for the menu container. |
aria-labelledby | string | — | Id of the element that labels the menu (alternative to aria-label). |
autoSearchThreshold | number | 13 | Item-count cutover for `searchable: 'auto'`. When the leaf-item count is STRICTLY greater than this, the search field appears. Default `13` — empirically the point where vertical scanning starts to slow down vs. typing a query. Counts nested submenu items recursively so a top-level menu with 6 groups × 4 children still triggers search. |
className | string | — | Merged onto the outer container. |
id | string | — | DOM id for the menu container. |
onAction | ((itemId: string) => void) | — | Fires when any leaf item (item without children, not a toggle) is selected anywhere in the menu tree. Receives the item id (or its array index if no id). Complements per-item `onSelect`. Bubbles from submenus to the root. |
onEscape | (() => void) | — | Fires when Escape is pressed anywhere in the tree. The menu does NOT close itself — its parent popover/portal owns visibility. |
onSearch | ((query: string) => void) | — | Fires on every search keystroke. Consumer filters `items`. |
searchable | boolean | "auto" | auto | Controls the inline search affordance at the top of the menu. - `true` — always show the search field - `false` — never show it - `'auto'` — show when the leaf-item count exceeds `autoSearchThreshold` (data-density scaling) - undefined — same as `'auto'` Default: `'auto'`. Lets a 5-item context menu stay tidy while a 30-item user-list menu opts users into filter-first navigation automatically. |
searchDefaultValue | string | — | Uncontrolled initial search value. |
searchPlaceholder | string | Search… | Search field placeholder. Default `'Search…'`. |
searchValue | string | — | Controlled search value. |
size | enum | m | Visual size. Default `'m'`. |
width | string | number | — | Fixed width override. Accepts a CSS length. |
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.
<MenuItem 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