---
id: "packages/active-ui/input-group"
title: "InputGroup"
description: "An input or textarea with icons, text, shortcuts or buttons attached inside its border."
url: "/packages/active-ui/input-group"
locale: "en"
revision: "ebb36c796984bd9e"
---

# InputGroup

An input group puts things inside the border of a text field: a search icon, a fixed prefix such as
`https://`, a unit, a keyboard shortcut, or a button that acts on the value, such as copy or clear.
The group draws one border and one focus state for all of it. For a field with a label, put the
group in a [Field](/packages/active-ui/field).

**Example: Icons, prefix and suffix, a button and a footer**

Client: Runs entirely in your browser. Source: `examples/ui/input-group.tsx`.

```tsx
// apps/docs/examples/ui/input-group.tsx
'use client';

import {useEffect, useRef, useState} from 'react';
import {CheckIcon, CopyIcon, SearchIcon} from 'lucide-react';

import {
  Field,
  FieldLabel,
} from '@workspace/active-ui/components/primitives/field';
import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupInput,
  InputGroupText,
  InputGroupTextarea,
} from '@workspace/active-ui/components/primitives/input-group';
import {Kbd} from '@workspace/active-ui/components/primitives/kbd';

const LINK = 'https://example.com/schemes/ps-2026';

/**
 * An input with something attached: an icon and a shortcut, a fixed prefix
 * and suffix, a button that acts on the value, and a textarea with a footer.
 * Clicking an addon that is not a button focuses the input. The shortcut
 * shown in the search field works: `/` focuses it, unless you are typing
 * in another field.
 */
export default function InputGroupExample() {
  const [copied, setCopied] = useState(false);
  const [message, setMessage] = useState('');
  const search = useRef<HTMLInputElement>(null);
  useEffect(() => {
    const onKey = (event: KeyboardEvent) => {
      const target = event.target as HTMLElement;
      if (event.key !== '/' || target.closest('input, textarea, select')) {
        return;
      }
      event.preventDefault();
      search.current?.focus();
    };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, []);
  const copy = async () => {
    await navigator.clipboard?.writeText(LINK).catch(() => undefined);
    setCopied(true);
    window.setTimeout(() => setCopied(false), 1500);
  };
  return (
    <div className="grid w-full max-w-md gap-6">
      <InputGroup>
        <InputGroupInput
          ref={search}
          placeholder="Search schemes"
          aria-label="Search schemes"
          aria-keyshortcuts="/"
        />
        <InputGroupAddon>
          <SearchIcon aria-hidden />
        </InputGroupAddon>
        <InputGroupAddon align="inline-end">
          <Kbd>/</Kbd>
        </InputGroupAddon>
      </InputGroup>

      <Field>
        <FieldLabel htmlFor="group-site">Website</FieldLabel>
        <InputGroup>
          <InputGroupAddon>
            <InputGroupText>https://</InputGroupText>
          </InputGroupAddon>
          <InputGroupInput id="group-site" placeholder="example.com" />
          <InputGroupAddon align="inline-end">
            <InputGroupText>/schemes</InputGroupText>
          </InputGroupAddon>
        </InputGroup>
      </Field>

      <Field>
        <FieldLabel htmlFor="group-link">Share link</FieldLabel>
        <InputGroup>
          <InputGroupInput id="group-link" value={LINK} readOnly />
          <InputGroupAddon align="inline-end">
            <InputGroupButton
              size="icon-xs"
              aria-label={copied ? 'Copied' : 'Copy link'}
              onClick={copy}
            >
              {copied ? <CheckIcon aria-hidden /> : <CopyIcon aria-hidden />}
            </InputGroupButton>
          </InputGroupAddon>
        </InputGroup>
      </Field>

      <Field>
        <FieldLabel htmlFor="group-message">Message</FieldLabel>
        <InputGroup>
          <InputGroupTextarea
            id="group-message"
            value={message}
            onChange={(event) => setMessage(event.target.value)}
            placeholder="Write a message"
          />
          <InputGroupAddon align="block-end" className="border-t">
            <InputGroupText>{message.length} characters</InputGroupText>
            <InputGroupButton
              variant="default"
              size="sm"
              className="ml-auto"
              disabled={message.trim() === ''}
              onClick={() => setMessage('')}
            >
              Send
            </InputGroupButton>
          </InputGroupAddon>
        </InputGroup>
      </Field>
    </div>
  );
}
```

## Usage

```tsx
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from '@workspace/active-ui/components/primitives/input-group';

<InputGroup>
  <InputGroupInput placeholder="Search" aria-label="Search" />
  <InputGroupAddon>
    <SearchIcon aria-hidden />
  </InputGroupAddon>
</InputGroup>;
```

## Anatomy

- `InputGroup`: the border, `role="group"`. It takes the focus ring of the control inside it, and
  the error border when that control has `aria-invalid`.
- `InputGroupInput` or `InputGroupTextarea`: the control, without a border of its own. Use these,
  not a plain `Input`: the group finds its control by them.
- `InputGroupAddon`: what is attached. `align` places it:

  - `inline-start` (the default) and `inline-end`: before or after the text, on the same line;
  - `block-start` and `block-end`: above or below it, across the full width, for a header or a
    toolbar. Add `border-b` or `border-t` to separate it.

  The order of the children does not matter; `align` decides. A click on an addon that is not a
  button focuses the input.
- `InputGroupText`: text or an icon in an addon, in the muted colour.
- `InputGroupButton`: a compact button in an addon, `ghost` and size `xs` by default, and
  `type="button"`, so it never submits the form by accident.

## What your application provides

- An accessible name for the control (`FieldLabel` or `aria-label`), and for every icon-only button.
- The behaviour of the buttons and of any shortcut you show. A `Kbd` in an addon only shows a key;
  the example binds `/` itself.
- The value; a prefix or suffix in an addon is not part of it.

## Accessibility and keyboard

- The control and each button are their own tab stops.
- A prefix or suffix is visible text, not part of the label. When it matters for understanding,
  such as a unit, repeat it in the label or the description.

## API

**InputGroup**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

InputGroup has no props of its own.

**InputGroupAddon**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `align` | `"block-end" \| "block-start" \| "inline-end" \| "inline-start" \| null` | no | Where the addon sits: `inline-start` and `inline-end` before or after the text on the same line, `block-start` and `block-end` above or below it across the full width. The order of the children does not matter. |

**InputGroupButton**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `size` | `"icon-sm" \| "icon-xs" \| "sm" \| "xs" \| null` | no | The size of the button inside the group: `xs` and `sm` with text, `icon-xs` and `icon-sm` for a single icon. |
| `type` | `"button" \| "reset" \| "submit"` | no | The button type. `button` by default, so a button inside a field never submits its form by accident. |
| `variant` | `"default" \| "destructive" \| "ghost" \| "link" \| "outline" \| "secondary" \| null` | no | The visual weight of the button. `default` for the main action of a screen or form; `outline` and `secondary` for other actions; `ghost` for a quiet action in a toolbar or a list; `destructive` for an action that deletes or cannot be undone; `link` for an action that reads as a link. Every variant but `link` sinks 1px while pressed; a link-styled action keeps its text and icon in place (`button-link-still`). |
| `focusableWhenDisabled` | `boolean` | no | Whether the button should be focusable when disabled. |
| `nativeButton` | `boolean` | no | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). |
| `render` | `ComponentRenderFn<HTMLProps, ButtonState> \| ReactElement<unknown, string \| JSXElementConstructor<any>>` | no | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

**InputGroupText**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

InputGroupText has no props of its own.

**InputGroupInput**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

InputGroupInput has no props of its own.

**InputGroupTextarea**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

InputGroupTextarea has no props of its own.

> [!NOTE]
> **Based on shadcn/ui**
>
> This page adapts the Input Group documentation of shadcn/ui (MIT licence) to
> this package; see [Third-party notices](/third-party-notices).
