Skip to documentation content

Input Group

Add visible context or actions around an input without separating them from the control.

input-group-search

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

npx honestui@latest add input-group

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.

input-group-url

Textarea with a block addon

A block-end addon holds guidance that would not fit beside the control.

input-group-textarea

Keyboard shortcut hint

A kbd element in an end addon advertises the shortcut; wiring the hint to real behavior keeps it honest.

examples/input-group-kbd.tsx
"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

PartRendersNotable props
InputGroupdiv with role="group"Native div props
InputGroupAddondiv with role="group"align: inline-start (default), inline-end, block-start, block-end; clicking it focuses the group's input unless a button was clicked
InputGroupButtonButtontype defaults to button; variant defaults to ghost; size: xs (default), sm, icon-xs, icon-sm
InputGroupTextspanNative span props; muted styling
InputGroupInputinputNative input props; borderless and stretched inside the group
InputGroupTextareatextareaNative textarea props; resizing is disabled so the group controls height

State attributes (aria-invalid, disabled) belong on the input or textarea; the container reacts to them through selectors.