Stems UI
Menu

Input

A text field for user input.

Type a name.

Codeapp/demos/public/input.tsx
import { clientEntry, type Handle } from 'remix/component'
import { Input } from '@stemsui/registry/ui/input.tsx'
import { Label } from '@stemsui/registry/ui/label.tsx'

export const InputDemo = clientEntry(import.meta.url, function InputDemo(handle: Handle) {
  let name = ''

  return () => (
    <div class="flex w-full max-w-sm flex-col gap-6">
      <div class="flex flex-col gap-2">
        <Label for="input-email">Email</Label>
        <Input id="input-email" type="email" placeholder="you@example.com" />
      </div>

      <div class="flex flex-col gap-2">
        <Label for="input-name">Name (controlled)</Label>
        <Input
          id="input-name"
          placeholder="Ada Lovelace"
          value={name}
          onValueChange={(value) => {
            name = value
            void handle.update()
          }}
        />
        <p class="text-sm text-muted-foreground" data-testid="input-greeting">
          {name ? `Hello, ${name}!` : 'Type a name.'}
        </p>
      </div>

      <div class="flex flex-col gap-2">
        <Label for="input-username">Username (uncontrolled, with default)</Label>
        <Input id="input-username" defaultValue="adalovelace" />
      </div>

      <div class="flex flex-col gap-2">
        <Label for="input-file">Picture</Label>
        <Input id="input-file" type="file" />
      </div>

      <Input aria-label="Disabled" placeholder="Disabled" disabled />
    </div>
  )
})

Installation

npx shadcn add @stemsui/input

Requires the one-time setup. Behavior: stems-ui/input.

API reference

The styled component is built on the headless module below: its props mirror these options, and the same events and data attributes apply.

stems-ui/input

input wires a native <input> (or <textarea> / <select>) with controlled/uncontrolled value handling and Base UI's state data attributes. Inside a field.Context it is the field's control.

Base UI equivalent: Input.

Usage

import { input } from 'stems-ui/input'

// Uncontrolled
;<input aria-label="Name" mix={input({ defaultValue: 'Ada' })} />

// Controlled
function NameField(handle: Handle) {
  let name = ''
  return () => (
    <input
      aria-label="Name"
      mix={input({
        value: name,
        onValueChange(value) {
          name = value
          void handle.update()
        },
      })}
    />
  )
}

Inside a field:

import * as field from 'stems-ui/field'

;<field.Context name="email" validationMode="onBlur">
  <label mix={field.label()}>Email</label>
  <input type="email" required mix={input()} />
  <div mix={field.error()}>Enter a valid email.</div>
</field.Context>

stems-ui/input

  • input(options?)
    • value: the value. Use when controlled. The owner must re-render with the next value.
    • defaultValue: the initial value. Use when uncontrolled.
    • onValueChange(value, event): called on every user edit (input event).
    • disabled: sets the native disabled attribute. Falls back to the host's disabled prop. An enclosing field.Context (or fieldset.Context) with disabled also disables it.
  • onInputChange(handler): listens for stems:input-change (InputChangeEvent, with value), which bubbles from the input after every user edit.

Behavior notes

  • Explicit host props win: a host value or defaultValue takes precedence over the options.
  • data-dirty compares against the value from the first render.
  • Base UI only sets data-focused, data-touched, data-dirty, and data-filled inside Field.Root. Stems sets them on a standalone input too, since they cost nothing and are useful for styling.
  • Inside a field.Context the field owns the state: the input renders the field's state attributes (adding data-valid / data-invalid once validated), the field's control id (the <label>'s for target), name, disabled, aria-invalid, and aria-describedby (descriptions and visible errors), and it reports focus, blur and edits to the field for validation. field.control() is this same mixin: use one or the other, not both.
  • Standalone, state changes between renders (focus, blur, typing) are written to the node directly instead of re-rendering the host. Inside a field, the field re-renders its parts.

Styling hooks

AttributeValues
data-disabledPresent when the input is disabled.
data-focusedPresent while the input has focus.
data-touchedPresent after the input has lost focus once.
data-dirtyPresent when the value differs from the initial value.
data-filledPresent when the value is not empty.
data-validInside a field: present when validated and valid.
data-invalidInside a field: present when invalid.