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

Search

Field / Component / Hero / List — 5 variants · 6 states.

Preview

Live preview
@oshon-ai/components
  • AVL-2841 (Avocado, large)
  • MGO-1107 (Mango, organic)
  • BNN-0921 (Banana, fair-trade)

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 search

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

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

Default

tsx
<Search size="m" placeholder="Search" aria-label="Demo search" />

Size matrix

tsx
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', alignItems: 'flex-start' }}>
      <div key="xs" data-story-size="xs">
        <Search size="xs" placeholder="Search" aria-label="Demo search" />
      </div>
      <div key="s" data-story-size="s">
        <Search size="s" placeholder="Search" aria-label="Demo search" />
      </div>
      <div key="m" data-story-size="m">
        <Search size="m" placeholder="Search" aria-label="Demo search" />
      </div>
      <div key="l" data-story-size="l">
        <Search size="l" placeholder="Search" aria-label="Demo search" />
      </div>
      <div key="mobile" data-story-size="mobile">
        <Search size="mobile" placeholder="Search" aria-label="Demo search" />
      </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
aria-labelstringaria-label for the field — falls back to "Search".
classNamestringAdditional classes merged after the root defaults.
defaultOpenbooleanUncontrolled initial open state.
defaultValuestringUncontrolled initial value.
disabledbooleanDisabled state — disables pointer events + dims text.
helperTextReactNodeHero-only: helper line under the field.
inputPropsOmit<InputHTMLAttributes<HTMLInputElement>, "onChange" | "defaultValue" | "size" | "value" | "placeholder" | "disabled">Forwarded to the inner `<input>` so consumers can pass a name, autocomplete hint, etc. without forking the component.
onOpenChange((open: boolean) => void)Called when the dropdown opens or closes.
onSegmentedClick(() => void)Hero-only: fires when the segmented chunk is clicked.
onSelectSuggestion((suggestion: SearchSuggestion) => void)Fires when a suggestion row is clicked.
onSubmit((value: string) => void)Hero-only: fires when the trailing CTA (search arrow) is clicked OR the user presses Enter while the input has focus. Field/component also fire on Enter, so this prop is shared.
onValueChange((value: string) => void)Called on every keystroke with the new value.
openbooleanControlled open (active/dropdown) state.
placeholderstringPlaceholder text. Defaults to "Search" (or "Search PI, MI, or SI..." for hero).
segmentedLabelReactNodeHero-only: leading "Segmented" group label (Figma renders "All"). Pass undefined to omit the segmented chunk.
sizeenummVisual size. Default `'m'`.
slotClassNameSearchSlotClassNamesPer-slot className overrides.
suggestionsreadonly SearchSuggestion[]Dropdown row data. When non-empty + `open` (or focus), the panel renders.
typeenumfieldVisual surface. Default `'field'`.
valuestringControlled value.

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

field

The interactive shell — border, background, focus ring, and inner gap. Carries `data-oshon-component="search"`, `data-oshon-type`, `data-oshon-size`, `data-oshon-open`, `data-oshon-disabled`, and `data-oshon-filled` for QA hooks.

icon

Leading magnifying-glass glyph. Inline SVG sized per the size token (12/14/16). Override via `slotClassName.icon` to swap the color or replace the glyph entirely.

input

The native `<input type="search">` element. Forwards all caller `inputProps` (autocomplete, name, etc.) but consumes `value`/`defaultValue`/`onChange`/`disabled`/`placeholder`/`size` so the controlled contract stays consistent.

clear

Trailing CloseAlt glyph that appears once the field has a value. `<button aria-label="Clear search">` — `mousedown.preventDefault` keeps focus on the input across the click.

dropdown

Suggestion panel rendered when `type="field"` + open + `suggestions.length > 0`. Frosted-glass chrome (rgba(223,226,226,0.4) + 16px backdrop-blur), 1px gray-300 border, 8px radius, drop-shadow + inner highlight. `role="listbox"` with `role="option"` children.

segmented

Hero-only leading "All" pivot button. Wraps the `segmentedLabel` content + a chevron-down glyph. Separated from the input by a 1px right border. Click fires `onSegmentedClick`.

cta

Hero-only trailing 40×40 white CTA button with the arrow-search glyph. Click (or Enter on the input) fires `onSubmit(value)`.

helper

Hero-only centered helper line (14/18 neutral-700) rendered below the pill when `helperText` is provided.

Keyboard

Field & Component: input is `<input type="search">` (implicit `role="searchbox"`); when `suggestions` is non-empty it surfaces `aria-autocomplete="list"` and (while open) `aria-controls` pointing at the dropdown listbox id. The dropdown is `role="listbox"`; rows are `role="option"`. Enter fires `onSubmit(value)`. Escape clears the value (first press) then closes the dropdown (second press). Trailing clear button is `<button aria-label="Clear search">` with `mousedown.preventDefault` so input focus is preserved across the click. Hero adds a `<button>` segmented chunk + a `<button aria-label="…submit">` CTA. Disabled cuts pointer events + applies opacity 0.6 + the native `disabled` attribute. Focus ring is `:focus-visible` only.

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

Do / Don't

✓ Do

Field with autocomplete suggestions
<Search
  suggestions={[
    { id: '1', label: 'Apple' },
    { id: '2', label: 'Banana' },
  ]}
  onSelectSuggestion={(s) => console.log(s.id)}
/>
Component-density search inside a toolbar
<Search type="component" placeholder="Search records" />
Hero search on a dashboard with segmented "All" filter
<Search
  type="hero"
  segmentedLabel="All"
  onSegmentedClick={() => {}}
  helperText="Search across all product data, queries, and management tools"
  onSubmit={(q) => navigate(`/search?q=${q}`)}
/>
Mobile field with explicit value control
<Search
  size="mobile"
  value={query}
  onValueChange={setQuery}
  placeholder="Search…"
/>

✗ Don't

Hardcoding a hex color on the field border
<Search slotClassName={{ field: "border-[#aaaaaa]" }} />

Bypassing the neutral-* tokens defeats white-label theming (principle #6). Override the slot only when you need to attach behavior (e.g. a hover ring); leave color decisions to the tokens.

Mixing controlled `value` with `defaultValue`
<Search value={query} defaultValue="seed" onValueChange={setQuery} />

Controlled and uncontrolled value sources fight for ownership and produce an unstable initial render. Pick one — `value` + `onValueChange` for controlled, `defaultValue` alone for uncontrolled.

Forcing `open=true` with an empty suggestions array
<Search open suggestions={[]} />

The dropdown panel only renders when `suggestions.length > 0`, regardless of the `open` prop. Passing `open` with an empty array signals an API misuse — either omit `suggestions` (no autocomplete) or supply rows.

Using `type="component"` on a white card surface
<div style={{ background: "white" }}><Search type="component" /></div>

Component density relies on `mix-blend-multiply` to tint a colored hosting surface; on a flat white background it reads identical to the field type without any of the bg/border affordances. Use `type="field"` on white cards; reserve `type="component"` for tinted toolbars and page headers.

Design rationale

Search is one of the highest-volume primitives in any product surface, so the design system needs a single component that can land on a form (field), a toolbar (component), or a dashboard (hero) without forking. The Figma source ships three named surfaces; we collapse them into a single `type` prop so the controlled-input + dropdown + clear-button + submit contract is shared. Sizes follow the standard five-axis convention: xs maps to the Component-density 24px / 10pt density (Figma's smallest published surface); s/m map to Field/Desktop 24px / 12pt; l/mobile map to Field/Mobile 34px / 14pt. Hero ignores the size axis for height (its chrome is fixed by Figma at 40×40 CTA + 16/8 inner padding) and only respects size for the helper text. The active-state dropdown intentionally reuses Popup chrome (frosted gray-300 wash + 16px backdrop-blur + drop-shadow-with-inner-highlight) so the same visual language carries across every floating surface in the system. Escape behaviour is two-stage — first press clears the typed value, second press closes the dropdown — matching the Mac OS convention users expect from Safari and Spotlight. The clear button uses `mousedown.preventDefault()` so the input focus returns to the input after the click; without that the click would blur the input and close the dropdown before the clear handler fires.