Input Group
Add visible context or actions around an input without separating them from the control.
Overview
Input Group places an input or textarea inside one shared border with supporting text, icons, or buttons. It is useful when the surrounding content changes how people interpret or operate the value, such as a fixed URL prefix, a search icon, or a clear button.
The group is visual structure, not a replacement for a field. Give the control a visible label and use Field when you also need a description, validation message, required indicator, or shared disabled state.
Anatomy
InputGroup is the outer container, rendered as a div with role="group". Add one InputGroupInput or InputGroupTextarea, then place InputGroupAddon before or after it. An addon can contain InputGroupText for non-interactive context or InputGroupButton for an action.
Inline addons sit at the start or end of a single-line input. Block addons sit above or below the control and let a textarea or input grow to its natural height — the container switches from a fixed 32 px line to an auto-height column whenever it detects a textarea or a block-aligned addon.
Behavior
Clicking anywhere in a non-interactive addon focuses the group's input, so the prefix area behaves as part of the field rather than dead space. Clicking an InputGroupButton keeps its own action instead. Note this focus hand-off targets <input> elements only; clicking a textarea's addon does not move focus into it.
The shared border reflects the control's state: focus shows a ring, aria-invalid="true" turns the border and ring to the danger color, and disabling the control dims the whole group. Put these attributes on the input or textarea itself, not on the container.
Prefix and suffix text is not submitted with the input value. If the server needs the complete value, combine the visible context with the submitted value in your application logic.
Accessibility
Keep a visible label outside the group, connected through htmlFor/id. The container's role="group" carries no name of its own, and addons are never announced as part of the field's name or description — a https:// prefix visible on screen does nothing for someone navigating by screen reader unless the accessible name covers it.
Decorative icons need aria-hidden="true"; icon-only buttons need an aria-label that names the action. Do not put required instructions or error messages only inside an addon, because they will not be associated with the control — pair the group with Field below it for descriptions and errors.
Keyboard users reach the input with Tab and type normally; the clear button and any other addon buttons are ordinary tab stops. The single-line group stands 32 px tall, clearing the WCAG 2.2 minimum target size of 24 px, though addon buttons are smaller and should stay easy to hit with surrounding spacing. Colors come from theme tokens, so focus rings, danger states, and dimmed disabled groups adapt to dark mode automatically.
Addon placement relies on flex ordering rather than fixed margins, so start and end addons swap sides correctly in right-to-left layouts; check the spacing next to each addon visually after localizing. Avoid stacking so many inline actions that the text entry area becomes too narrow — long values already scroll horizontally inside the control.
Installation
Usage
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
} from "@/components/ui/input-group";<InputGroup>
<InputGroupAddon>
<InputGroupText>https://</InputGroupText>
</InputGroupAddon>
<InputGroupInput aria-label="Website address" name="website" />
</InputGroup>Don't do this
Letting the addon act as the label
// Bad
<InputGroup>
<InputGroupAddon>
<InputGroupText>honestui.com/</InputGroupText>
</InputGroupAddon>
<InputGroupInput name="slug" />
</InputGroup>// Good
<Label htmlFor={id}>Project URL</Label>
<InputGroup>
<InputGroupAddon>
<InputGroupText>honestui.com/</InputGroupText>
</InputGroupAddon>
<InputGroupInput id={id} name="slug" />
</InputGroup>The prefix explains the value's shape but never becomes its name: the field is announced as "edit text" (or by placeholder alone), leaving screen reader users to guess what belongs there. Keep a real label outside the group; the addon adds context on top of it.
Unnamed icon buttons in an addon
// Bad
<InputGroupButton size="icon-xs" onClick={clear}>
<XIcon />
</InputGroupButton>// Good
<InputGroupButton size="icon-xs" aria-label="Clear search" onClick={clear}>
<XIcon aria-hidden="true" />
</InputGroupButton>An icon-only button has no accessible name, so assistive technology announces just "button" and voice-control users have nothing to say to trigger it. Name every icon-only addon action, and mark its icon decorative.
Examples
These examples focus on the relationships that are unique to Input Group: an action beside a control, contextual text that is not part of the value, and a block addon below a textarea.
Prefix and suffix
Both sides add context; only the slug itself is submitted.
Textarea with a block addon
A block-end addon holds guidance that would not fit beside the control.
Keyboard shortcut hint
A kbd element in an end addon advertises the shortcut; wiring the hint to real behavior keeps it honest.
"use client";
import * as React from "react";
import { Search as SearchIcon } from "honestui/icons";
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/components/ui/input-group";
export default function InputGroupKbd() {
const inputRef = React.useRef<HTMLInputElement>(null);
React.useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
event.preventDefault();
inputRef.current?.focus();
}
}
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, []);
return (
<div className="w-full max-w-sm space-y-2">
<label className="text-sm font-medium" htmlFor="command-query">
Search commands
</label>
<InputGroup>
<InputGroupAddon>
<SearchIcon aria-hidden="true" />
</InputGroupAddon>
<InputGroupInput
ref={inputRef}
id="command-query"
type="search"
placeholder="Jump to a component"
/>
<InputGroupAddon align="inline-end">
<kbd className="bg-muted text-muted-foreground pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border px-1.5 font-mono text-[10px] font-medium">
⌘K
</kbd>
</InputGroupAddon>
</InputGroup>
</div>
);
}
API reference
State attributes (aria-invalid, disabled) belong on the input or textarea; the container reacts to them through selectors.