Input
Text inputs and a textarea whose ref is the real element, so react-hook-form works without a Controller.
View as Markdown for coding assistants, or see llms.txt.
- Stable
- Group: Inputs
- Added in @azmr/ui 0.2.0
Usage
import { Input, Textarea } from "@azmr/ui/input";<div className="flex w-full max-w-sm flex-col gap-2">
<label htmlFor="demo-input-email" className="text-sm font-medium">
Email
</label>
<Input
id="demo-input-email"
type="email"
placeholder="aroha@example.co.nz"
autoComplete="email"
/>
</div>Every input needs a visible label. Wrap it in a Field, which adds the label, description and error and wires them up, or use your own <label htmlFor> (or aria-label when the design has no label).
States
Enter a valid email address.
import { Input } from "@azmr/ui/input";
export default function InputStates() {
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<div className="flex flex-col gap-2">
<label htmlFor="demo-input-disabled" className="text-sm font-medium">
Disabled
</label>
<Input id="demo-input-disabled" disabled defaultValue="Locked" />
</div>
<div className="flex flex-col gap-2">
<label htmlFor="demo-input-readonly" className="text-sm font-medium">
Read only
</label>
<Input id="demo-input-readonly" readOnly defaultValue="Can focus, cannot edit" />
</div>
<div className="flex flex-col gap-2">
<label htmlFor="demo-input-invalid" className="text-sm font-medium">
Invalid
</label>
<Input
id="demo-input-invalid"
aria-invalid="true"
aria-describedby="demo-input-invalid-message"
defaultValue="not-an-email"
/>
<p id="demo-input-invalid-message" className="text-sm text-destructive">
Enter a valid email address.
</p>
</div>
</div>
);
}Inside a Field, pass error and this is done for you. On its own, aria-invalid="true" styles the error state and is what assistive technology announces; point aria-describedby at the message so it is read with the field. A read-only input stays focusable and can be selected and copied, while a disabled one is skipped by Tab.
Textarea
With autoResize, no scrollbar appears.
import { Field } from "@azmr/ui/field";
import { Textarea } from "@azmr/ui/input";
export default function InputTextarea() {
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<Field label="Message">
<Textarea placeholder="Drag the corner to resize." />
</Field>
<Field label="Grows as you type" description="With autoResize, no scrollbar appears.">
<Textarea autoResize rows={2} />
</Field>
</div>
);
}Textarea is a real <textarea> with the same styling, states and value API as Input. It shows three rows by default and resizes vertically; autoResize makes it grow with its content instead (CSS field-sizing: content; browsers without it keep the fixed height).
For a currency, a unit, a search icon or a clear button inside the border, use Input group.
Debounce and commit
Searches 300 ms after you stop typing.
0 searches: Hamilton East, Hamilton Lake, Hillcrest, Frankton, Rototuna, Chartwell
Saved when you leave the field or press Enter.
<div className="flex w-full max-w-sm flex-col gap-4">
<Field label="Suburb" description="Searches 300 ms after you stop typing.">
<Input
type="search"
debounceMs={300}
onValueChange={(value) => {
setQuery(value);
setSearches((count) => count + 1);
}}
className="data-[pending]:bg-muted/40"
/>
</Field>
<p className="text-sm text-muted-foreground">
{searches === 1 ? "1 search" : `${searches} searches`}: {matches.join(", ") || "no matches"}
</p>
<Field label="Business name" description="Saved when you leave the field or press Enter.">
<Input defaultValue="Northline Electrical" onCommit={setSaved} />
</Field>
<p role="status" className="text-sm text-muted-foreground">
{saved ? `Saved "${saved}".` : ""}
</p>
</div>debounceMs makes onValueChange wait until typing stops, for a search or a filter. onCommit fires when the person is done with the value: when they leave the field or press Enter. Use it for autosave.
- On blur or Enter, a waiting
onValueChangeis sent straight away, thenonCommit. - An unchanged value is never reported twice: typing a letter and deleting it within the wait sends nothing, and leaving the field again doesn't commit again.
data-pendingis set while a value is waiting, for styling (data-[pending]:…).- A waiting value is dropped if the input unmounts.
- The native
onChangestill fires on every keystroke, soregister()from React Hook Form keeps the form state current. - Enter doesn't commit while an input method is composing (Japanese, Chinese or Korean input), where Enter picks a candidate.
The same behaviour comes as hooks from @azmr/ui/hooks for your own components: useDebouncedCallback(callback, delayMs) returns a stable function with flush(), cancel() and isPending(), and useDebouncedValue(value, delayMs) returns the value once it has settled.
With react-hook-form
The ref is the real <input>, so register() works directly:
import { useForm } from "react-hook-form";
import { Input } from "@azmr/ui/input";
function EmailField() {
const { register } = useForm<{ email: string }>();
return <Input type="email" {...register("email")} />;
}Native onChange and a typed onValueChange(value) both fire. Textarea works the same way. To show the error from formState.errors, see Field with react-hook-form.
Props
Prop
Type
All native <input> attributes pass through. className is merged last, so it can override the defaults, and onValueChange gives you the new value directly instead of event.target.value.
Height and horizontal padding follow the density tokens, so the input lines up with Button at every density.
In an example
The settings page example validates a whole form built from inputs, and the form validation recipe shows the pattern on a single field.
Accessibility
- A real
<input>: name, role and value come from the platform. - The focus ring is full strength (WCAG 2.2 focus appearance), never faded.
- Invalid state is exposed through
aria-invalid, not colour alone: pair it with a text message.
| Key | Action |
|---|---|
| Tab | Moves focus into and out of the field. Disabled inputs are skipped. |
| ShiftorTab | Moves focus to the previous control. |
| Esc | Not captured, so a parent dialog can use it to close. |