AZMARA PLATFORM
Components

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.

Usage

import { Input, Textarea } from "@azmr/ui/input";
Preview theme
Preview density
<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>
Open as text

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

Preview theme
Preview density

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

Preview theme
Preview density

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

Preview theme
Preview density

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>
Open as text

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:

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.
Keyboard interactions
KeyAction
TabMoves focus into and out of the field. Disabled inputs are skipped.
ShiftorTabMoves focus to the previous control.
EscNot captured, so a parent dialog can use it to close.