# Input Group

> A React input group that joins a field with text addons, selects, icons and buttons into one box with one focus ring and one error state.

- Type: React component (Inputs)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add input-group`
- Docs: https://wingo-ui.com/components/input-group

## Interaction

Several controls read as one field: the whole row takes a single soft ring when any part has focus, a single red ring when it is invalid, and dims as one when disabled. Tab walks through the controls in order. A click or tap on a text addon like https:// or kg lands the caret in the input next to it; a click on the label focuses the first control. Selects inside open their list as usual, as a bottom sheet below 768px. A solid button at the end (Apply) sits flush with the rounded corner and shows its spinner while loading without changing width. On a phone the row stays on one line down to 320px: addons never wrap, the input shrinks, text is 16px so iOS never zooms and the md row grows to 44px.

## Installation

```bash
npx wingo-ui@latest add input-group
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/input-group` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/input-group.tsx`

npm dependencies: `npm i @radix-ui/react-slot`

Wingo UI dependencies (installed automatically):

- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { useState } from "react"
import { Input } from "@/components/ui/input"
import { InputGroup, InputGroupAddon, InputGroupButton } from "@/components/ui/input-group"
import { Select } from "@/components/ui/select"

const DOMAINS = [
  { value: ".com", label: ".com" },
  { value: ".io", label: ".io" },
  { value: ".dev", label: ".dev" },
]

export function SiteAddress() {
  const [domain, setDomain] = useState(".com")
  const [pending, setPending] = useState(false)

  return (
    <div className="flex flex-col gap-4">
      <InputGroup label="Website address" description="The name without spaces.">
        <InputGroupAddon>https://</InputGroupAddon>
        <Input aria-label="Domain" placeholder="northwind" name="domain" />
        <Select aria-label="Extension" options={DOMAINS} value={domain} onValueChange={(next) => next && setDomain(next)} />
      </InputGroup>

      <InputGroup label="Discount code" error="This coupon has expired">
        <Input aria-label="Coupon code" placeholder="Coupon code" />
        <InputGroupButton
          variant="solid"
          loading={pending}
          onClick={() => {
            setPending(true)
            setTimeout(() => setPending(false), 1200)
          }}
        >
          Apply
        </InputGroupButton>
      </InputGroup>
    </div>
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label?` | `ReactNode` | `""` | Names the whole row (the group is aria-labelledby it); a click on it focuses the first control. The controls inside stay bare and take an aria-label each (Domeniu, Extensie). |
| `description?` | `ReactNode` | `""` | Helper text under the row. It describes the group and every control inside (aria-describedby), together with the text addons. |
| `error?` | `ReactNode` | `""` | Replaces the description with the animated error and turns the whole row red; every control inside gets aria-invalid. |
| `required?` | `boolean` | `false` | The red mark after the label; the controls inside get required. |
| `invalid?` | `boolean` | `false` | The red ring and aria-invalid on every control without a message. |
| `disabled?` | `boolean` | `false` | Dims the row once to 50% and disables every control and button inside. |
| `readOnly?` | `boolean` | `false` | Values stay visible and selectable but cannot change; selects do not open and the row drops its hover tint. The demo fills in a sample value. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | Height 32 / 40 / 48px (md 44px on touch). Pass it to the group only: the controls, addons and buttons inside follow it. |
| `variant?` | `"soft" \| "outline"` | `"soft"` | soft is the filled well, outline a bordered box. Ghost and underline make no sense for a joined row. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Corner shape of the row, scaled with the size like the button's (md is 8 / 10 / 12px); full makes a pill. Inner parts are clipped to it. |
| `separators?` | `boolean` | `true` | Hairlines between segments (controls, filled addons, ghost buttons). Plain addons are text inside a segment and a solid button is its own edge, so they never get one. |
| `children?` | `ReactNode` |  | InputGroupAddon, bare W2 controls (Input, Select, NumberInput, CurrencyInput... with no label of their own, an aria-label each) and InputGroupButton, in order. The row never wraps (the input shrinks instead); on a phone keep it to one or two controls plus addons, and split longer rows into separate groups. |
| `classNames?` | `Partial<Record<"field" \| "label" \| "description" \| "error" \| "box" \| "separator", string>>` |  | Extra classes for the parts: field is the wrapper with the label, box the joined row. |
| `className?` | `string` |  | Classes for the outermost element: the wrapper when there is a label, description or error, else the row. |

## Parts

### InputGroupAddon (component)

Text or an icon at either end. position is inferred from the order when omitted. variant "plain" (default) is muted text that hugs the input, "filled" a tinted segment like a classic addon. A click or tap focuses the nearest input (links and buttons inside keep their own click). asChild renders a <label htmlFor>. Text addons (USD, kg) join the controls' aria-describedby; describe={false} opts out. Props: ComponentProps<"span"> & { position?: "start" | "end"; variant?: "plain" | "filled"; asChild?: boolean; describe?: boolean }.

### InputGroupButton (component)

A Button flush with the row: variant "ghost" and radius "none" by default, full height, the group's size (an explicit sm / md / lg sets its own text size), inset focus outline, disabled with the group. size="icon" makes it square; loading keeps its width. Props: ButtonProps.

### InputGroupSeparator (component)

A 1px hairline placed by hand, for example with separators={false}. Decorative (aria-hidden). Takes span props.

## Examples

- **Price**: A CurrencyInput joined with a currency Select: one box, one ring, and the amount reformats for USD or EUR.
- **Coupon**: A solid Apply button flush with the end: it checks the code for 1.2s, then SUMMER24 shows the error in red and FALL25 applies the discount.
- **Weight and size**: A plain kg addon that reads as text inside the box, and a filled cm segment like a classic addon.
- **Search with category**: A pill search bar: category Select, the query and an icon button that runs a pretend search. Below 768px the category opens as a bottom sheet.
- **Country code**: A filled +1 segment before a tel input. For real phone numbers use phone-input.
- **Sizes and variants**: sm, md and lg in soft and outline, lined up with a plain Input under each set.

## See also

- [Input](https://wingo-ui.com/components/input) ([Markdown](https://wingo-ui.com/components/input.md))
- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))
- [Button Group](https://wingo-ui.com/components/button-group) ([Markdown](https://wingo-ui.com/components/button-group.md))
- [Select](https://wingo-ui.com/components/select) ([Markdown](https://wingo-ui.com/components/select.md))
- [Currency Input](https://wingo-ui.com/components/currency-input) ([Markdown](https://wingo-ui.com/components/currency-input.md))

---

Canonical: https://wingo-ui.com/components/input-group
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
