1
ComponentsForm Controls

Input Group

Input Group combines an input or textarea with tightly related icons, units, text, shortcuts, or inline actions.

Purpose

Input Group combines an input or textarea with tightly related icons, units, text, keyboard shortcuts, or inline actions.

Affixed inputs

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

export function Example() {
  return (
    <InputGroup>
      <InputGroupAddon>https://</InputGroupAddon>
      <InputGroupInput aria-label="Domain" placeholder="domain.com" />
    </InputGroup>
  );
}

Installation

With the Siteplane registry alias configured, add the component and import the installed source from your app.

CLI

pnpm dlx shadcn@4.16.1 add @siteplane/input-group

Import

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

Need the machine-readable source metadata? View registry JSON.

Usage

Use Input Group for prefixes, suffixes and merged text areas. Do not fake affixes with absolute-positioned local markup.

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

export function Example() {
  return (
    <InputGroup>
      <InputGroupAddon>https://</InputGroupAddon>
      <InputGroupInput aria-label="Domain" placeholder="domain.com" />
    </InputGroup>
  );
}

Examples

Icons, text affixes, action addons, variants, sizes, states and form wiring are compositions of the same primitives.

With icon

Clicking an addon focuses the field; interactive children are excluded.

{/* Leading icon */}
<InputGroup>
  <InputGroupAddon>
    <SearchIcon />
  </InputGroupAddon>
  <InputGroupInput aria-label="Search projects" placeholder="Search projects" />
</InputGroup>

{/* Trailing icon */}
<InputGroup>
  <InputGroupInput aria-label="Workspace ID" placeholder="acme-prod" />
  <InputGroupAddon align="inline-end">
    <HashIcon />
  </InputGroupAddon>
</InputGroup>

Text affixes

https://
.com
{/* Prefix text */}
<InputGroup>
  <InputGroupAddon>
    <InputGroupText>https://</InputGroupText>
  </InputGroupAddon>
  <InputGroupInput aria-label="Website URL" placeholder="siteplane-ui" />
</InputGroup>

{/* Suffix text */}
<InputGroup>
  <InputGroupInput aria-label="Subdomain" placeholder="acme" />
  <InputGroupAddon align="inline-end">
    <InputGroupText>.com</InputGroupText>
  </InputGroupAddon>
</InputGroup>

Currency

EUR
<InputGroup>
  <InputGroupAddon>
    <EuroIcon />
  </InputGroupAddon>
  <InputGroupInput aria-label="Monthly amount" inputMode="decimal" placeholder="2,500" />
  <InputGroupAddon align="inline-end">
    <InputGroupText>EUR</InputGroupText>
  </InputGroupAddon>
</InputGroup>

With button

const { copyPhase, isShowingCheck, playCopyFeedback } = useCopyIconPhase();

<InputGroup>
  <InputGroupInput aria-label="Invite link" readOnly value="https://ui.siteplane.io/i/9f2a" />
  <InputGroupAddon align="inline-end">
    <Button
      aria-label={isShowingCheck ? "Invite link copied" : "Copy invite link"}
      onClick={handleCopy}
      size="icon-xs"
      type="button"
      variant="ghost"
    >
      <CopyRedrawIcon phase={copyPhase} />
    </Button>
  </InputGroupAddon>
</InputGroup>

Password toggle

const [visible, setVisible] = useState(false);

<InputGroup>
  <InputGroupInput
    aria-label="Password"
    placeholder="Password"
    type={visible ? "text" : "password"}
  />
  <InputGroupAddon align="inline-end">
    <Button
      aria-label={visible ? "Hide password" : "Show password"}
      aria-pressed={visible}
      onClick={() => setVisible((prev) => !prev)}
      size="icon-xs"
      type="button"
      variant="ghost"
    >
      <RevealIcon revealed={visible} />
    </Button>
  </InputGroupAddon>
</InputGroup>

Textarea group

Markdown supported

Shown in the workspace changelog.

<Field>
  <FieldLabel id={labelId}>Release note</FieldLabel>
  <InputGroup>
    <InputGroupTextarea aria-labelledby={labelId} placeholder="Describe what changed..." />
    <InputGroupAddon align="block-end">
      <InputGroupText>Markdown supported</InputGroupText>
    </InputGroupAddon>
  </InputGroup>
  <FieldDescription>Shown in the workspace changelog.</FieldDescription>
</Field>

Soft variant

<InputGroup>
  <InputGroupAddon><SearchIcon /></InputGroupAddon>
  <InputGroupInput aria-label="Default variant search" placeholder="Default" />
</InputGroup>

<InputGroup variant="soft">
  <InputGroupAddon><SearchIcon /></InputGroupAddon>
  <InputGroupInput aria-label="Soft variant search" placeholder="Soft" />
</InputGroup>

Small size

The group adjusts addon padding when the input renders at size sm.

https://
<InputGroup>
  <InputGroupAddon>
    <InputGroupText>https://</InputGroupText>
  </InputGroupAddon>
  <InputGroupInput aria-label="Small site URL" placeholder="siteplane-ui" size="sm" />
</InputGroup>

Disabled

A disabled input dims the whole group including addons.

<InputGroup>
  <InputGroupAddon>
    <SearchIcon />
  </InputGroupAddon>
  <InputGroupInput aria-label="Disabled search" disabled placeholder="Search disabled" />
</InputGroup>

Field with error

shop/
.ui.siteplane.io
Enter a handle between the prefix and suffix.
<Field>
  <FieldLabel>Store handle</FieldLabel>
  <InputGroup aria-invalid>
    <InputGroupAddon>
      <InputGroupText>shop/</InputGroupText>
    </InputGroupAddon>
    <InputGroupInput aria-invalid aria-label="Store handle" defaultValue="" />
    <InputGroupAddon align="inline-end">
      <InputGroupText>.ui.siteplane.io</InputGroupText>
    </InputGroupAddon>
  </InputGroup>
  <FieldError match>Enter a handle between the prefix and suffix.</FieldError>
</Field>

Form integration

Query: not submitted
<Form onSubmit={onSubmit}>
  <Field>
    <FieldLabel>Search workspace</FieldLabel>
    <InputGroup>
      <InputGroupAddon>
        <SearchIcon />
      </InputGroupAddon>
      <InputGroupInput aria-label="Search query" name="q" placeholder="Type and submit" />
      <InputGroupAddon align="inline-end">
        <Button
          aria-busy={loading}
          aria-label="Submit search"
          disabled={loading}
          size="icon-xs"
          type="submit"
          variant="ghost"
        >
          {loading ? <Spinner className="size-4" /> : <SendIcon className="size-4" />}
        </Button>
      </InputGroupAddon>
    </InputGroup>
  </Field>
  <span className="text-muted-foreground text-xs">{status}</span>
</Form>

API Reference

The reference lists the source-owned exports and props that are easy to miss in visual examples. Inherited Base UI props remain available unless the wrapper narrows them.

  • InputGroupAddon align accepts "inline-start", "inline-end", "block-start", or "block-end". Block start places the addon row above the field.
  • Use InputGroupText for non-interactive prefixes, suffixes, and units.
  • Kbd may be placed in an addon and receives the native radius and spacing treatment.
  • Put real Button controls in an addon for copy, reveal, submit, or composer actions. Textarea groups include native Button layout for action rows.
  • Use Input Group only when the addon is tightly coupled to the field value.

Motion

Component motion uses the shared Siteplane motion contract. See the global motion guide for provider setup, tokens and reduced-motion behavior.

  • Focus and invalid rings wrap the complete group.
  • Empty addon space may focus the field; an interactive child keeps its own Button action.
  • The group itself does not animate, and invalid or disabled state must not change its dimensions.

Accessibility

  • Give every input or textarea a visible FieldLabel, aria-label, or aria-labelledby.
  • Every interactive addon Button needs its own accessible name.
  • Supporting text, icons, units, and Kbd hints must not become the field's only label.
  • Connect invalid state to the field and render a visible FieldError.

Implementation Guidance

  • Use align instead of custom addon layout.
  • Use Input for a plain field without closely related addons.
  • Preserve focus routing, group rings, textarea action layout, and child control semantics.

On This Page