# Input

> Text inputs and a textarea whose ref is the real element, so react-hook-form works without a Controller.

Source: https://platform.azmara.io/docs/components/input

Group: Inputs · Status: stable · Added in @azmr/ui 0.2.0

## Usage

```tsx
import { Input, Textarea } from "@azmr/ui/input";
```

```tsx
import { Input } from "@azmr/ui/input";

export default function InputBasic() {
  return (
    <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`](https://platform.azmara.io/docs/components/field.md), 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

```tsx
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`](https://platform.azmara.io/docs/components/field.md), 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

```tsx
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](https://platform.azmara.io/docs/components/input-group.md).

## Debounce and commit

```tsx
"use client";

import { Field } from "@azmr/ui/field";
import { Input } from "@azmr/ui/input";
import { useState } from "react";

const SUBURBS = [
  "Hamilton East",
  "Hamilton Lake",
  "Hillcrest",
  "Frankton",
  "Rototuna",
  "Chartwell",
];

export default function InputDebounce() {
  const [query, setQuery] = useState("");
  const [searches, setSearches] = useState(0);
  const [saved, setSaved] = useState<string>();
  const matches = SUBURBS.filter((name) => name.toLowerCase().includes(query.toLowerCase()));

  return (
    <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 `onValueChange` is sent straight away, then `onCommit`.
- 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-pending` is set while a value is waiting, for styling (`data-[pending]:…`).
- A waiting value is dropped if the input unmounts.
- The native `onChange` still fires on every keystroke, so `register()` 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:

```tsx
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](https://platform.azmara.io/docs/components/field.md#with-react-hook-form).

## Props

| Prop            | Type                                                                             | Default | Required | Description                                                                                                                                                                                                                      |
| --------------- | -------------------------------------------------------------------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `onValueChange` | `((value: string, eventDetails: Input.ChangeEventDetails) => void) \| undefined` |         | No       | Callback fired when the `value` changes. Use when controlled.                                                                                                                                                                    |
| `defaultValue`  | `string \| number \| readonly string[] \| undefined`                             |         | No       | The default value of the input. Use when uncontrolled.                                                                                                                                                                           |
| `value`         | `string \| number \| readonly string[] \| undefined`                             |         | No       | The value of the input. Use when controlled.                                                                                                                                                                                     |
| `className`     | `string`                                                                         |         | No       |                                                                                                                                                                                                                                  |
| `debounceMs`    | `number`                                                                         |         | No       | Waits this long after the last keystroke before calling `onValueChange`, for a search or an autosave. The native `onChange` (and so `register()`) still fires on every keystroke. `data-pending` is set while a call is waiting. |
| `onCommit`      | `((value: string) => void) \| undefined`                                         |         | No       | Called when the person is done with the value: on blur, or on Enter. It first sends any waiting `onValueChange`. It is not called again for a value it has already reported.                                                     |

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](https://platform.azmara.io/docs/examples/settings-page.md) validates a whole form built from inputs, and the [form validation recipe](https://platform.azmara.io/docs/recipes/form-validation.md) 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. |
| `Shift` or `Tab` | Moves focus to the previous control.                                |
| `Esc`            | Not captured, so a parent dialog can use it to close.               |
