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/inputRequires 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 (inputevent).disabled: sets the nativedisabledattribute. Falls back to the host'sdisabledprop. An enclosingfield.Context(orfieldset.Context) withdisabledalso disables it.
onInputChange(handler): listens forstems:input-change(InputChangeEvent, withvalue), which bubbles from the input after every user edit.
Behavior notes
- Explicit host props win: a host
valueordefaultValuetakes precedence over the options. data-dirtycompares against the value from the first render.- Base UI only sets
data-focused,data-touched,data-dirty, anddata-filledinsideField.Root. Stems sets them on a standalone input too, since they cost nothing and are useful for styling. - Inside a
field.Contextthe field owns the state: the input renders the field's state attributes (addingdata-valid/data-invalidonce validated), the field's controlid(the<label>'sfortarget),name,disabled,aria-invalid, andaria-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
| Attribute | Values |
|---|---|
data-disabled | Present when the input is disabled. |
data-focused | Present while the input has focus. |
data-touched | Present after the input has lost focus once. |
data-dirty | Present when the value differs from the initial value. |
data-filled | Present when the value is not empty. |
data-valid | Inside a field: present when validated and valid. |
data-invalid | Inside a field: present when invalid. |