Siteplane UI

Input Group

Attach text, icons or actions to an input surface.

Markdown
"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

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

Complete the one-time setup first. Registry JSON lists source files and dependencies.

For this page’s examples, also install:

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

Field is installed with Input.

Usage

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

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

Composition

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.

https://
€
EUR
"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 with profile="field-action".

"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.

"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, 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.

Visible to your team
"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.

"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

Component properties
PropTypeDefaultDescription
InputGroup.variantdefault | elevated | inset | softdefaultShared surface.
InputGroup.textSizesm | baseinferred from lg/xl child; otherwise CSSText size.
InputGroup.insetEndbooleanfalseKeeps 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.onMouseDownMouseEventHandlerunsetCalled before focus forwarding; preventDefault cancels forwarding.
InputGroupAddon.aligninline-start | inline-end | block-start | block-endinline-startPosition in the group.
InputGroupAddon.dividerbooleanfalseDraws a border between the addon and the control.
InputGroupAddon.paddingdefault | compact | nonedefaultcompact reduces the addon's outer inline padding; none removes its padding.
InputGroupSegment.width2-digit | 4-digit | fill2-digitSegment width.
InputGroupSegment.insetStartbooleanfalseInset start edge.
InputGroupSegment.onFocusFocusEventHandlerunsetCalled 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.

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.