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

Split Field

L/R split text field — e.g. price + currency.

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 splitfield

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

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

Basic — controlled

Include area code.

US · (empty)
tsx
<BasicField />

State matrix — Desktop (m)

Enabled (empty)

Optional Description

Filled

Optional Description

DropdownActive

Optional Description

ErrorFilled

DisabledFilled

Optional Description

tsx
<div style={stackCol}>
      <p style={sectionHeading}>Enabled (empty)</p>
      <SplitField
        label="Label"
        required
        tooltip
        description="Optional Description"
        dropdownOptions={COUNTRIES}
        dropdownAriaLabel="C"
        placeholder="555-1212"
      />

      <p style={sectionHeading}>Filled</p>
      <SplitField
        label="Label"
        required
        tooltip
        description="Optional Description"
        dropdownOptions={COUNTRIES}
        dropdownAriaLabel="C"
        defaultValue="555-1212"
      />

      <p style={sectionHeading}>DropdownActive</p>
      <SplitField
        label="Label"
        required
        tooltip
        description="Optional Description"
        dropdownOptions={COUNTRIES}
        dropdownAriaLabel="C"
        dropdownActive
        defaultValue="555-1212"
      />

      <p style={sectionHeading}>ErrorFilled</p>
      <SplitField
        label="Label"
        required
        tooltip
        error="Mandatory Error Description"
        dropdownOptions={COUNTRIES}
        dropdownAriaLabel="C"
        defaultValue="555-1212"
      />

      <p style={sectionHeading}>DisabledFilled</p>
      <SplitField
        label="Label"
        required
        tooltip
        description="Optional Description"
        dropdownOptions={COUNTRIES}
        dropdownAriaLabel="C"
        defaultValue="555-1212"
        disabled
      />
    </div>

State matrix — Mobile

Enabled

Optional Description

Filled

Optional Description

ErrorFilled

tsx
<div style={stackCol}>
      <p style={sectionHeading}>Enabled</p>
      <SplitField
        size="mobile"
        label="Label"
        required
        tooltip
        description="Optional Description"
        dropdownOptions={COUNTRIES}
        dropdownAriaLabel="C"
        placeholder="555-1212"
      />
      <p style={sectionHeading}>Filled</p>
      <SplitField
        size="mobile"
        label="Label"
        required
        tooltip
        description="Optional Description"
        dropdownOptions={COUNTRIES}
        dropdownAriaLabel="C"
        defaultValue="555-1212"
      />
      <p style={sectionHeading}>ErrorFilled</p>
      <SplitField
        size="mobile"
        label="Label"
        required
        tooltip
        error="Mandatory Error Description"
        dropdownOptions={COUNTRIES}
        dropdownAriaLabel="C"
        defaultValue="555-1212"
      />
    </div>

Field position — right (currency + amount)

fieldPosition="right" puts the dropdown on the left and the input on the right — matches Figma's "Right" variant.

tsx
<div style={stackCol}>
      <SplitField
        label="Amount"
        fieldPosition="right"
        dropdownOptions={CURRENCIES}
        dropdownAriaLabel="Currency"
        defaultValue="0.00"
        description={`fieldPosition="right" puts the dropdown on the left and the input on the right — matches Figma's "Right" variant.`}
      />
    </div>

Field position — left vs right (side-by-side)

fieldPosition="left" (default) — input on left, dropdown on right

fieldPosition="right" — dropdown on left, input on right

tsx
<div style={stackCol}>
      <p style={sectionHeading}>fieldPosition="left" (default) — input on left, dropdown on right</p>
      <SplitField
        label="Phone"
        dropdownOptions={COUNTRIES}
        dropdownAriaLabel="Country"
        defaultValue="555-1212"
      />
      <p style={sectionHeading}>fieldPosition="right" — dropdown on left, input on right</p>
      <SplitField
        label="Phone"
        fieldPosition="right"
        dropdownOptions={COUNTRIES}
        dropdownAriaLabel="Country"
        defaultValue="555-1212"
      />
    </div>

Size ladder — xs → mobile

Proportional to Figma scale.

Proportional to Figma scale.

Proportional to Figma Desktop.

Proportional to Figma scale.

Proportional to Figma Mobile.

tsx
<SizeLadderPlayground />

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

label

Field label (ReactNode). Desktop: Lato Medium 12/16 neutral-600. Mobile: Lato Regular 14/18 neutral-600.

tooltip

Optional info-icon slot next to the required asterisk.

secondaryAction

Right-justified slot inside the label row.

dropdownOptions

Array of `{ value, label }` — rendered as native <option> children of the dropdown half.

description

Helper message under the field. Suppressed when `error` is present.

error

Error message under the field. When truthy, sets aria-invalid=true on the input and overrides description.

Keyboard

Tab/Shift+Tab follows DOM order — for fieldPosition="left" (default), Tab moves input → dropdown; for fieldPosition="right", Tab moves dropdown → input. Native <select> + <input> keyboard behavior. Label associated with the input half; dropdown has its own aria-label. Description + error wired via aria-describedby on the input.

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-05-09

Do / Don't

✓ Do

Phone (country + number)
<SplitField label="Phone" dropdownOptions={[{value:"us",label:"US"},{value:"uk",label:"UK"}]} placeholder="555-1212" />
Amount with currency on the left (input on the right)
<SplitField label="Amount" fieldPosition="right" dropdownOptions={currencies} placeholder="0.00" />
Required with description
<SplitField label="Phone" required description="Include area code." dropdownOptions={countries} />
Error
<SplitField label="Phone" dropdownOptions={countries} error="Invalid number." />

✗ Don't

Composing two fields manually
<Dropdown options={[…]} /><TextField label="Number" />

Loses the shared shell, the 1px divider, the hover/focus-within state propagation, and the SplitField Figma palette. A SplitField reads as one field; stacked components read as two.

Forwarding native `required` + `aria-invalid`
<SplitField required aria-required aria-invalid />

SplitField drives aria-required from `required` and aria-invalid from `error` / `invalid`. Manual wiring fights the state machine.

Rendering both description AND error
<SplitField label="X" description="Help" error="Bad" dropdownOptions={[]} />

Figma contract: description xor error. When `error` is present, description is suppressed.

Design rationale

Phase 4e.3 visual fidelity refit. Pixel-literal port of Figma DS 3.1 SplitField component set (node 5986:16009) — all 36 variants. Shell owns the border + state machine so the dropdown + input read as a single field even when only one half is focused. Divider is a 1px vertical bar sized to 50% of the field height (12px at m desktop, 17px at mobile/l) per Figma. `dropdownActive` expresses the docked/open state: divider + chevron flip to primary-700 and chevron rotates.