AZMARA PLATFORM
Components

Input group

Text, icons and buttons inside an input's border, such as a currency, a unit, a search icon or a clear or copy button.

View as Markdown for coding assistants, or see llms.txt.

Usage

import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@azmr/ui/input-group";
Preview theme
Preview density

Excluding GST.

NZ$
per hour
kg
https://
<div className="flex w-full max-w-sm flex-col gap-4">
  <Field label="Hourly rate" description="Excluding GST.">
    <InputGroup>
      <InputGroupAddon>NZ$</InputGroupAddon>
      <InputGroupInput name="rate" inputMode="decimal" placeholder="0.00" />
      <InputGroupAddon>per hour</InputGroupAddon>
    </InputGroup>
  </Field>
  <Field label="Parcel weight">
    <InputGroup>
      <InputGroupInput name="weight" inputMode="decimal" />
      <InputGroupAddon>kg</InputGroupAddon>
    </InputGroup>
  </Field>
  <Field label="Website">
    <InputGroup>
      <InputGroupAddon>https://</InputGroupAddon>
      <InputGroupInput name="website" placeholder="example.co.nz" />
    </InputGroup>
  </Field>
</div>
Open as text

InputGroup draws one border around an InputGroupInput and the addons and buttons beside it. The focus ring wraps the whole group, the height follows the density tokens, and the border turns red when the input is invalid. Put addons and buttons before or after the input, in the order they appear.

Addons are not part of the input's value: the NZ$ above is shown, not submitted. Pressing an addon focuses the input, as pressing a label would.

Text and icon addons

A text addon carries meaning, so it is added to the input's accessible description: a screen reader hears "Hourly rate, NZ$, per hour, Excluding GST". It comes ahead of the field's own description and error.

An icon is decoration, so an addon that holds anything other than plain text is hidden from screen readers. Set describe to change either default, for example describe on an addon that wraps its text in <abbr>, or describe={false} on text that only repeats the label.

Buttons

Preview theme
Preview density

5 customers

  • Aroha Ngata
  • Mere Tāwhiri
  • James Wilson
  • Priya Patel
  • Liam O'Connor
<div className="flex w-full max-w-sm flex-col gap-3">
  <Field label="Search customers">
    <InputGroup>
      <InputGroupAddon>
        <Icon icon={Search} />
      </InputGroupAddon>
      <InputGroupInput
        ref={input}
        type="search"
        value={query}
        onValueChange={setQuery}
        placeholder="Name"
      />
      {query ? (
        <InputGroupButton
          size="icon"
          aria-label="Clear search"
          onClick={() => {
            setQuery("");
            // Back to the input, so the next search starts where the last one did.
            input.current?.focus();
          }}
        >
          <Icon icon={X} />
        </InputGroupButton>
      ) : null}
    </InputGroup>
  </Field>
  <p role="status" className="text-sm text-muted-foreground">
    {matches.length === 1 ? "1 customer" : `${matches.length} customers`}
  </p>
  <ul className="flex flex-col gap-1 text-sm">
    {matches.map((name) => (
      <li key={name}>{name}</li>
    ))}
  </ul>
</div>
Open as text

InputGroupButton is a Button in the ghost style, small enough to fit inside the border. It is type="button", so it never submits the form, and it comes after the input in the tab order. An icon-only button needs an aria-label, such as "Clear search".

After a clear button empties the input, move focus back to the input, as the demo does, so keyboard users don't lose their place.

Preview theme
Preview density

Safe to use in the browser.

<div className="flex w-full max-w-sm flex-col gap-4">
  <Field label="Publishable key" description="Safe to use in the browser.">
    <InputGroup>
      <InputGroupInput readOnly value={KEY} className="font-mono" />
      <InputGroupButton size="icon" aria-label={copied ? "Copied" : "Copy key"} onClick={copy}>
        <Icon icon={copied ? Check : Copy} />
      </InputGroupButton>
    </InputGroup>
  </Field>
  <Field label="Discount code">
    <InputGroup>
      <InputGroupInput value={code} onValueChange={setCode} autoComplete="off" />
      <InputGroupButton
        variant="secondary"
        disabled={code.trim() === ""}
        onClick={() => setApplied(code.trim().toUpperCase())}
      >
        Apply
      </InputGroupButton>
    </InputGroup>
  </Field>
  <p role="status" className="text-sm text-muted-foreground">
    {applied ? `${applied} applied.` : ""}
  </p>
</div>
Open as text

In a Field and with react-hook-form

Put the InputGroup inside a Field. The label, description and error are wired to the input as they are for a bare Input, and the aria-required and aria-errormessage that Field sets reach the input, not the group. InputGroupInput's ref is the real <input>, so register() works without a Controller:

<Field label="Amount" required error={errors.amount?.message}>
  <InputGroup>
    <InputGroupAddon>NZ$</InputGroupAddon>
    <InputGroupInput
      inputMode="decimal"
      {...register("amount", { required: "Enter an amount" })}
    />
  </InputGroup>
</Field>

For amounts, the formatted MoneyField (coming in a later release) will take the same addons.

Props

InputGroupAddon

Prop

Type

InputGroupInput takes every Input prop, and InputGroupButton takes every Button prop, with variant="ghost", size="sm" and type="button" as defaults. InputGroup takes className and the props of a <div>.

Accessibility

  • The input keeps its label, description and error wiring; text addons join its description, and icon addons are hidden from screen readers.
  • Buttons are real <button type="button"> elements in the tab order after the input; an icon-only button needs an aria-label.
  • The focus ring wraps the whole group when the input has focus, and each button shows its own ring.
  • Invalid state is shown by the border colour and by the field's error text, never by colour alone.
Keyboard interactions
KeyAction
TabMoves focus to the input, then to each button in the group.
ShiftorTabMoves focus back through the buttons to the input.
EnterOn a button, activates it without submitting the form.