# Input Group

An input with icons, text or buttons attached.

Source: [input-group.tsx](https://github.com/jakejarvis/jarvis-ui/blob/main/src/components/ui/input-group.tsx)

```tsx
import { SearchIcon } from "lucide-react";

import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group";
import { Kbd } from "@/components/ui/kbd";

export default function InputGroupWithIcon() {
  return (
    <InputGroup className="max-w-sm">
      <InputGroupInput placeholder="Search projects" aria-label="Search projects" />
      <InputGroupAddon>
        <SearchIcon />
      </InputGroupAddon>
      <InputGroupAddon align="inline-end">
        <Kbd>⌘K</Kbd>
      </InputGroupAddon>
    </InputGroup>
  );
}
```

## Installation

Add the registry, once per project:

```bash
npx shadcn@latest registry add @jarvis=https://ui.jarv.is/r/{name}.json
```

Then add the component:

```bash
npx shadcn@latest add @jarvis/input-group
```

## Usage

```tsx
import { SearchIcon } from "lucide-react";

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

export function SearchInput() {
  return (
    <InputGroup>
      <InputGroupInput placeholder="Search" aria-label="Search" />
      <InputGroupAddon>
        <SearchIcon />
      </InputGroupAddon>
    </InputGroup>
  );
}
```

Put the control first and the addons after it. Each addon's `align` places it around the control.
Label the control itself, with an `aria-label` or a `FieldLabel` in a [Field](https://ui.jarv.is/components/field.md).

## Composition

- `InputGroup`
  - `InputGroupInput`
  - `InputGroupTextarea`
  - `InputGroupAddon`
    - `InputGroupText`
    - `InputGroupButton`

## Examples

### With a spinner

An addon can hold any small element, such as a [Spinner](https://ui.jarv.is/components/spinner.md).

```tsx
import { AtSignIcon } from "lucide-react";

import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group";
import { Spinner } from "@/components/ui/spinner";

export default function InputGroupWithSpinner() {
  return (
    <InputGroup className="max-w-sm">
      <InputGroupInput placeholder="ada" aria-label="Username" />
      <InputGroupAddon>
        <AtSignIcon />
      </InputGroupAddon>
      <InputGroupAddon align="inline-end">
        <Spinner />
      </InputGroupAddon>
    </InputGroup>
  );
}
```

### With text

`InputGroupText` adds a prefix or suffix, such as a currency or a unit.

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

export default function InputGroupWithText() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-4">
      <InputGroup>
        <InputGroupInput placeholder="acme" aria-label="Subdomain" />
        <InputGroupAddon>
          <InputGroupText>https://</InputGroupText>
        </InputGroupAddon>
        <InputGroupAddon align="inline-end">
          <InputGroupText>.example.com</InputGroupText>
        </InputGroupAddon>
      </InputGroup>
      <InputGroup>
        <InputGroupInput inputMode="decimal" placeholder="0.00" aria-label="Monthly budget" />
        <InputGroupAddon>
          <InputGroupText>$</InputGroupText>
        </InputGroupAddon>
        <InputGroupAddon align="inline-end">
          <InputGroupText>USD</InputGroupText>
        </InputGroupAddon>
      </InputGroup>
      <InputGroup>
        <InputGroupInput inputMode="numeric" defaultValue="25" aria-label="Seat limit" />
        <InputGroupAddon>
          <InputGroupText>Seat limit</InputGroupText>
        </InputGroupAddon>
      </InputGroup>
    </div>
  );
}
```

### With buttons

`InputGroupButton` is a small button that fits the group. Give icon-only buttons an `aria-label`.

```tsx
import { CopyIcon, GlobeIcon } from "lucide-react";

import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupInput,
} from "@/components/ui/input-group";
import { toast } from "@/components/ui/toast";

export default function InputGroupWithButton() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-4">
      <InputGroup>
        <InputGroupInput
          readOnly
          defaultValue="https://example.com/share/website-redesign"
          aria-label="Share link"
        />
        <InputGroupAddon>
          <GlobeIcon />
        </InputGroupAddon>
        <InputGroupAddon align="inline-end">
          <InputGroupButton
            size="icon-xs"
            aria-label="Copy link"
            onClick={() => toast.add({ title: "Link copied" })}
          >
            <CopyIcon />
          </InputGroupButton>
        </InputGroupAddon>
      </InputGroup>
      <InputGroup>
        <InputGroupInput placeholder="Filter by name" aria-label="Filter" />
        <InputGroupAddon align="inline-end">
          <InputGroupButton>Search</InputGroupButton>
        </InputGroupAddon>
      </InputGroup>
    </div>
  );
}
```

### Invalid

Set `aria-invalid` on the control to outline the whole group.

```tsx
import { InfoIcon, MailIcon } from "lucide-react";

import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupInput,
} from "@/components/ui/input-group";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";

export default function InputGroupInvalid() {
  return (
    <InputGroup className="max-w-sm">
      <InputGroupInput aria-invalid defaultValue="sam@example" aria-label="Email" />
      <InputGroupAddon>
        <MailIcon />
      </InputGroupAddon>
      <InputGroupAddon align="inline-end">
        <Tooltip>
          <TooltipTrigger
            render={<InputGroupButton size="icon-xs" aria-label="Why is this invalid?" />}
          >
            <InfoIcon />
          </TooltipTrigger>
          <TooltipContent>Enter a full email address</TooltipContent>
        </Tooltip>
      </InputGroupAddon>
    </InputGroup>
  );
}
```

### With a textarea

Use `InputGroupTextarea` in place of the input. Align addons to `block-start` or `block-end` to put
them above or below it.

```tsx
import { ArrowUpIcon, PaperclipIcon } from "lucide-react";
import * as React from "react";

import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupText,
  InputGroupTextarea,
} from "@/components/ui/input-group";
import { toast } from "@/components/ui/toast";

export default function InputGroupWithTextarea() {
  const [message, setMessage] = React.useState("");
  return (
    <InputGroup className="max-w-md">
      <InputGroupTextarea
        placeholder="Leave a comment…"
        aria-label="Comment"
        value={message}
        onChange={(event) => setMessage(event.target.value)}
        maxLength={280}
      />
      <InputGroupAddon align="block-end">
        <InputGroupButton size="icon-xs" aria-label="Attach a file">
          <PaperclipIcon />
        </InputGroupButton>
        <InputGroupText className="ml-auto tabular-nums">{message.length}/280</InputGroupText>
        <InputGroupButton
          variant="secondary"
          size="icon-xs"
          aria-label="Send"
          disabled={!message.trim()}
          focusableWhenDisabled
          onClick={() => {
            toast.add({ title: "Comment posted", type: "success" });
            setMessage("");
          }}
        >
          <ArrowUpIcon />
        </InputGroupButton>
      </InputGroupAddon>
    </InputGroup>
  );
}
```

### Large

Set `size="lg"` for a taller group.

```tsx
import { Button } from "@/components/ui/button";
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
  InputGroupText,
} from "@/components/ui/input-group";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { toast } from "@/components/ui/toast";

const paymentMethods = { visa: "Visa •• 7391", amex: "Amex •• 1008" };

export default function InputGroupLarge() {
  return (
    <div className="flex w-full max-w-md flex-wrap items-center gap-2">
      <InputGroup size="lg" className="w-auto min-w-40 flex-1">
        <InputGroupInput inputMode="decimal" defaultValue="50.00" aria-label="Amount in USD" />
        <InputGroupAddon>
          <InputGroupText>$</InputGroupText>
        </InputGroupAddon>
      </InputGroup>
      <Select items={paymentMethods} defaultValue="visa">
        <SelectTrigger size="lg" aria-label="Payment method">
          <SelectValue />
        </SelectTrigger>
        <SelectContent>
          {Object.entries(paymentMethods).map(([value, label]) => (
            <SelectItem key={value} value={value}>
              {label}
            </SelectItem>
          ))}
        </SelectContent>
      </Select>
      <Button
        variant="secondary"
        size="lg"
        onClick={() => toast.add({ title: "Funds added", type: "success" })}
      >
        Add funds
      </Button>
    </div>
  );
}
```

## Props

`InputGroupInput` and `InputGroupTextarea` are an [Input](https://ui.jarv.is/components/input.md) and a
[Textarea](https://ui.jarv.is/components/textarea.md).

| Part | Extends |
| --- | --- |
| `InputGroup` | `<div>` |
| `InputGroupAddon` | `<div>` |
| `InputGroupButton` | [`Button`](https://base-ui.com/react/components/button#api-reference) |
| `InputGroupText` | `<span>` |
| `InputGroupInput` | `<input>` |
| `InputGroupTextarea` | `<textarea>` |

The tables leave out `className`, `style` and `render`. Parts built on Base UI take them as its [styling](https://base-ui.com/react/handbook/styling) and [composition](https://base-ui.com/react/handbook/composition) guides describe.

### InputGroup

| Prop    | Type                | Description          |
| ------- | ------------------- | -------------------- |
| `size?` | `"default" \| "lg"` | Default: `"default"` |

### InputGroupAddon

| Prop     | Type                                                             | Description               |
| -------- | ---------------------------------------------------------------- | ------------------------- |
| `align?` | `"block-end" \| "block-start" \| "inline-end" \| "inline-start"` | Default: `"inline-start"` |

### InputGroupButton

| Prop                     | Type                                                                          | Description                                                                                                                                                                                       |
| ------------------------ | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `nativeButton?`          | `boolean`                                                                     | 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>`). Default: `true` |
| `focusableWhenDisabled?` | `boolean`                                                                     | Whether the button should be focusable when disabled. Default: `false`                                                                                                                            |
| `variant?`               | `"destructive" \| "ghost" \| "link" \| "outline" \| "primary" \| "secondary"` | Default: `"ghost"`                                                                                                                                                                                |
| `size?`                  | `"icon-sm" \| "icon-xs" \| "sm" \| "xs"`                                      | Default: `"xs"`                                                                                                                                                                                   |
| `type?`                  | `"button" \| "reset" \| "submit"`                                             | Default: `"button"`                                                                                                                                                                               |
