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

Input

Lowest-level Input primitive — TextField/TextArea/SplitField compose this.

Preview

Live preview
@oshon-ai/components

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 input

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

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

Default

tsx
<Input size="m" placeholder="Type here" aria-label="Demo input" />

Size matrix

tsx
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', alignItems: 'flex-start' }}>
      <div key="xs" data-story-size="xs">
        <Input size="xs" placeholder="Type here" aria-label="Demo input" />
      </div>
      <div key="s" data-story-size="s">
        <Input size="s" placeholder="Type here" aria-label="Demo input" />
      </div>
      <div key="m" data-story-size="m">
        <Input size="m" placeholder="Type here" aria-label="Demo input" />
      </div>
      <div key="l" data-story-size="l">
        <Input size="l" placeholder="Type here" aria-label="Demo input" />
      </div>
      <div key="mobile" data-story-size="mobile">
        <Input size="mobile" placeholder="Type here" aria-label="Demo input" />
      </div>
    </div>

Permission denied

tsx
<Input size="m" permissions={{ can: () => false }} placeholder="Type here" aria-label="Demo input" />

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

None. Native <input> is a void element; use `value` / `defaultValue` + `placeholder` for its content.

Keyboard

Tab/Shift+Tab: focus. All native keyboard behavior for <input>. aria-invalid + aria-disabled + aria-readonly synchronised by the primitive.

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

Basic
<Input placeholder="Name" onChange={handleChange} />
Controlled + invalid
<Input value={email} onChange={(e) => setEmail(e.target.value)} invalid={!emailValid} aria-describedby="email-error" />
Read-only gated by permission
<Input resource="field:ssn" permissions={{ mode: "readOnly" }} value={ssn} />
Size
<Input size="l" placeholder="Larger field" />

✗ Don't

Using a raw <input>
<input value={v} onChange={fn} />

Loses permission/audit plumbing, the Oshon focus ring, the invalid/disabled/readOnly visual contract, and the data-oshon-size hook. Always use <Input>.

Hardcoded border color override
<Input className="border-blue-500" />

Breaks white-labeling. If you need a different surface state (e.g. success), add a variant to the manifest and compose it via tokens.

Design rationale

Phase 4b form-field anchor, refit in 4e.3 to Figma DS 3.1 TextField component set (node 5945:991). Size heights: xs/s/m/l/mobile = h20/h22/h24/h28/h32 (m = Figma Desktop, mobile = Figma Mobile, others proportional). 4px radius, 1px border. State machine locked to Figma literal hex: enabled neutral-400 (#c6cbcb), hover primary-400 (≈Blue Stone 500 #62bdbd), focus primary-700 (Blue Stone 700 #038487 exact), invalid error-600 (oops-700 #e3362b exact), disabled transparent border + neutral-100 bg + neutral-600 text. Focus + invalid live on the same box (outline-none + box-shadow ring + border color shift) so focus never causes layout shift.