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.
- New
- Group: Inputs
- Added in @azmr/ui 0.6.0
Usage
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@azmr/ui/input-group";Excluding GST.
<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>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
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>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.
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>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 anaria-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.
| Key | Action |
|---|---|
| Tab | Moves focus to the input, then to each button in the group. |
| ShiftorTab | Moves focus back through the buttons to the input. |
| Enter | On a button, activates it without submitting the form. |