# Input Group

Attach text, icons or actions to an input surface.

Search with icon

[Complete TSX file](/examples/input-group/default.tsx)

```tsx
"use client";

import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/siteplane/components/input-group";
import { SearchIcon } from "lucide-react";

export default function Example() {
  return (
    <InputGroup className="max-w-sm">
      <InputGroupInput
        aria-label="Search teams or members"
        placeholder="Search teams or members…"
        type="search"
      />
      <InputGroupAddon>
        <SearchIcon />
      </InputGroupAddon>
    </InputGroup>
  );
}
```

## Installation

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/input-group
```

With npm: `npx shadcn@4.18.0 add @siteplane/input-group`. [Registry JSON](/registry/input-group.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/action-icons @siteplane/button @siteplane/input
```

Field is installed with Input.

## Usage

```tsx
import {
  InputGroup,
  InputGroupInput,
} from "@/siteplane/components/input-group";

<InputGroup>
  <InputGroupInput aria-label="Search" />
</InputGroup>;
```

## Composition

```text
InputGroup
├── InputGroupAddon
│   └── InputGroupText or Button
├── InputGroupInput / InputGroupTextarea / InputGroupSegment
└── InputGroupAddon
```

## With text

Put InputGroupText in an addon for a fixed prefix or suffix, such as a protocol or currency. Addon text is not part of the submitted value.

Website and budget with text addons

[Complete TSX file](/examples/input-group/with-text.tsx)

```tsx
"use client";

import { Field, FieldLabel } from "@/siteplane/components/field";
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
  InputGroupText,
} from "@/siteplane/components/input-group";

export default function Example() {
  return (
    <div className="grid w-full max-w-sm gap-4">
      <Field>
        <FieldLabel>Website</FieldLabel>
        <InputGroup>
          <InputGroupInput placeholder="example.com" />
          <InputGroupAddon>
            <InputGroupText>https://</InputGroupText>
          </InputGroupAddon>
        </InputGroup>
      </Field>
      <Field>
        <FieldLabel>Monthly budget</FieldLabel>
        <InputGroup>
          <InputGroupInput inputMode="decimal" placeholder="2,500" />
          <InputGroupAddon>
            <InputGroupText>€</InputGroupText>
          </InputGroupAddon>
          <InputGroupAddon align="inline-end">
            <InputGroupText>EUR</InputGroupText>
          </InputGroupAddon>
        </InputGroup>
      </Field>
    </div>
  );
}
```

## With a button

Place a Button in an inline-end addon for an action on the value. `divider` separates it from the input and `padding="compact"` fits a text button. An icon-only action (show or hide, copy, clear, open a picker) uses `profile="field-action"` with `shape="square"`: a bare icon without a box or hover background that darkens on hover and press, with the focus ring tight around the icon. For a copy action, use [Copy Button](/docs/components/copy-button/) with `profile="field-action"`.

Discount code with an apply button

[Complete TSX file](/examples/input-group/addons.tsx)

```tsx
"use client";

import { Button } from "@/siteplane/components/button";
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/siteplane/components/input-group";

export default function Example() {
  return (
    <InputGroup className="max-w-sm">
      <InputGroupInput aria-label="Discount code" defaultValue="WELCOME10" />
      <InputGroupAddon align="inline-end" divider padding="compact">
        <Button variant="ghost">Apply</Button>
      </InputGroupAddon>
    </InputGroup>
  );
}
```

## Password visibility

Toggle the input type with an icon Button on the `field-action` profile. Keep its label constant and expose the state with `aria-pressed`.

Password with visibility toggle

[Complete TSX file](/examples/input-group/password-toggle.tsx)

```tsx
"use client";

import { RevealIcon } from "@/siteplane/components/action-icons";
import { Button } from "@/siteplane/components/button";
import { Field, FieldLabel } from "@/siteplane/components/field";
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/siteplane/components/input-group";
import { useState } from "react";

export default function Example() {
  const [visible, setVisible] = useState(false);

  return (
    <Field className="w-full max-w-sm">
      <FieldLabel>Password</FieldLabel>
      <InputGroup>
        <InputGroupInput
          autoComplete="current-password"
          type={visible ? "text" : "password"}
        />
        <InputGroupAddon align="inline-end">
          <Button
            aria-label="Show password"
            aria-pressed={visible}
            profile="field-action"
            shape="square"
            onClick={() => setVisible((current) => !current)}
          >
            <RevealIcon revealed={visible} />
          </Button>
        </InputGroupAddon>
      </InputGroup>
    </Field>
  );
}
```

## Block addons

Block-aligned addons place a header or footer inside a textarea group. InputGroupTextarea grows with its content like [Textarea](/docs/components/textarea/), so it shows no resize grip where the browser supports `field-sizing: content`. For an action that sits over the textarea, such as a square Button in its top-end corner, set `insetEnd` on InputGroup: the text then ends 8 px before a Button of the control size.

Comment box with footer

[Complete TSX file](/examples/input-group/multiline.tsx)

```tsx
"use client";

import { Button } from "@/siteplane/components/button";
import {
  InputGroup,
  InputGroupAddon,
  InputGroupText,
  InputGroupTextarea,
} from "@/siteplane/components/input-group";

export default function Example() {
  return (
    <InputGroup className="max-w-sm">
      <InputGroupTextarea aria-label="Comment" placeholder="Write a comment…" />
      <InputGroupAddon align="block-end" divider>
        <InputGroupText>Visible to your team</InputGroupText>
        <Button className="ms-auto" size="sm">
          Comment
        </Button>
      </InputGroupAddon>
    </InputGroup>
  );
}
```

## Surfaces

The group owns the shared surface. Inner input/textarea wrappers are unstyled.

Input group surfaces

[Complete TSX file](/examples/input-group/appearance.tsx)

```tsx
"use client";

import { Field, FieldLabel } from "@/siteplane/components/field";
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/siteplane/components/input-group";
import { SearchIcon } from "lucide-react";

export default function Example() {
  return (
    <div className="grid w-full max-w-sm gap-4">
      {(["default", "elevated", "inset", "soft"] as const).map((variant) => (
        <Field key={variant}>
          <FieldLabel>Search, {variant}</FieldLabel>
          <InputGroup variant={variant}>
            <InputGroupInput
              placeholder="Search teams or members…"
              type="search"
            />
            <InputGroupAddon>
              <SearchIcon />
            </InputGroupAddon>
          </InputGroup>
        </Field>
      ))}
    </div>
  );
}
```

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| InputGroup.variant | default \| elevated \| inset \| soft | default | Shared surface. |
| InputGroup.textSize | sm \| base | inferred from lg/xl child; otherwise CSS | Text size. |
| InputGroup.insetEnd | boolean | false | Keeps the end of the input or textarea clear for an action placed over it, such as a square Button 8 px inside the corner. |
| InputGroup.onMouseDown | MouseEventHandler | unset | Called before focus forwarding; preventDefault cancels forwarding. |
| InputGroupAddon.align | inline-start \| inline-end \| block-start \| block-end | inline-start | Position in the group. |
| InputGroupAddon.divider | boolean | false | Draws a border between the addon and the control. |
| InputGroupAddon.padding | default \| compact \| none | default | compact reduces the addon's outer inline padding; none removes its padding. |
| InputGroupSegment.width | 2-digit \| 4-digit \| fill | 2-digit | Segment width. |
| InputGroupSegment.insetStart | boolean | false | Inset start edge. |
| InputGroupSegment.onFocus | FocusEventHandler | unset | Called after the native input text is selected. |
InputGroup accepts native div props, default role group. Clicking
non-interactive space forwards focus to an enabled input, textarea or
spinbutton. InputGroupAddon accepts div props; InputGroupText accepts span
props. InputGroupInput and InputGroupSegment accept InputProps except
`unstyled`, and InputGroupTextarea accepts TextareaProps except `unstyled`: the
group draws the field surface, so the inner control is always unstyled. Inner
size, state, events and form props remain those of Input/Textarea. All parts
accept string className. There is no InputGroupButton export; compose Button
directly.

[Read the installed source](/source/input-group).

## Usage notes

Addon text is not automatically an input label or description. Use Label/Field and aria-describedby as appropriate. The group does not manage values, disable all children, validate data or submit a form.
