Folio

Components

Input Group

An Input with attached context: a leading or trailing icon, a prefix or suffix, or a single action, all inside one visual boundary so the field still reads as a single control.

Input Group is a composed pattern built on the Input primitive. It does not replace Input. It extends it for the cases where attached context is required.

Preview

Rendered with live Folio tokens. Focus it and the whole group border changes color, no dark: classes.

Variants

<InputGroup>
  <InputGroupInput placeholder="Search..." />
  <InputGroupAddon>
    <Search />
  </InputGroupAddon>
</InputGroup>
<InputGroup>
  <InputGroupInput defaultValue="alex@distyl.ai" />
  <InputGroupAddon align="inline-end">
    <Check className="text-success" />
  </InputGroupAddon>
</InputGroup>
https://
<InputGroup>
  <InputGroupInput placeholder="workspace" />
  <InputGroupAddon>
    <InputGroupText>https://</InputGroupText>
  </InputGroupAddon>
</InputGroup>
USD
<InputGroup>
  <InputGroupInput placeholder="0.00" />
  <InputGroupAddon align="inline-end">
    <InputGroupText>USD</InputGroupText>
  </InputGroupAddon>
</InputGroup>
<InputGroup>
  <InputGroupInput placeholder="Invite by email" />
  <InputGroupAddon align="inline-end">
    <InputGroupButton variant="outline" size="sm" className="h-7 gap-2 rounded-sm text-xs shadow [&_svg:not([class*='size-'])]:size-3.5">
      <Send />
    </InputGroupButton>
  </InputGroupAddon>
</InputGroup>
<InputGroup>
  <InputGroupInput placeholder="username" />
  <InputGroupAddon>
    <AtSign />
  </InputGroupAddon>
  <InputGroupAddon align="inline-end">
    <InputGroupButton variant="outline" size="sm" className="h-7 gap-2 rounded-sm text-xs shadow [&_svg:not([class*='size-'])]:size-3.5">
      Add
    </InputGroupButton>
  </InputGroupAddon>
</InputGroup>

Each InputGroupAddon holds one kind of context: an icon, an InputGroupText prefix or suffix, or an InputGroupButton. Set align to put it at the start or the end. Use one addon on each side to pair an icon with an action.

States

Default. Empty and at rest.

Focused. The whole group rings (shown statically here).

Filled. Holds a value.

<InputGroup>
  <InputGroupInput aria-invalid defaultValue="not a domain" />
  <InputGroupAddon>
    <Globe />
  </InputGroupAddon>
</InputGroup>
<InputGroup>
  <InputGroupInput disabled defaultValue="folio" />
  <InputGroupAddon>
    <Globe />
  </InputGroupAddon>
</InputGroup>

The group reads its state from the control. Focus on InputGroupInput changes the whole boundary, not only the inner field. aria-invalid swaps the border to the danger token, and disabled dims the whole group.

API

Part
Key prop
Default
Description
InputGroup
div props
—
The single visual boundary. It takes its focus, error, and disabled look from the control inside it.
InputGroupAddon
"inline-start" | "inline-end" | "block-start" | "block-end"
"inline-start"
align -- where the attached context sits. Holds an icon, InputGroupText, or InputGroupButton. A click on it focuses the input.
InputGroupText
span props
—
Static prefix or suffix text, for example a protocol, a unit, or a domain.
InputGroupInput
Input props
—
The field. Set aria-invalid for the error state and disabled to dim the group. All native input props pass through.
InputGroupTextarea
textarea props
—
A multi-line field in place of InputGroupInput. The group grows to fit it.
InputGroupButton
"xs" | "sm" | "icon-xs" | "icon-sm"
"xs"
size -- an action attached inside the boundary. Takes Button variant (default "ghost") and type (default "button").

Order the parts in JSX as you like: each addon places itself with align. Put InputGroupInput first so the tab order reaches the field before any action.

Don't and Do

Don't

Don't stack multiple trailing actions in one group. Two or more competing buttons crowd the field and blur which one the value belongs to. When more than one action is needed, move them out of the field into a different pattern.

Do
<InputGroup>
  <InputGroupInput placeholder="Search the workspace..." />
  <InputGroupAddon>
    <Search />
  </InputGroupAddon>
  <InputGroupAddon align="inline-end">
    <InputGroupButton variant="outline" size="sm" className="h-7 gap-2 rounded-sm text-xs shadow [&_svg:not([class*='size-'])]:size-3.5">
      Search
    </InputGroupButton>
  </InputGroupAddon>
</InputGroup>

Use it to add contextual affordance to an input without breaking the field's visual boundary.

import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
  InputGroupText,
} from "@/components/shadcn/input-group";
import { Globe } from "lucide-react";

export function SiteField() {
  return (
    <InputGroup>
      <InputGroupInput placeholder="workspace" />
      <InputGroupAddon>
        <Globe />
        <InputGroupText>https://</InputGroupText>
      </InputGroupAddon>
      <InputGroupAddon align="inline-end">
        <InputGroupText>.distyl.ai</InputGroupText>
      </InputGroupAddon>
    </InputGroup>
  );
}
API matches @distylai/toolkit-ui -- compound parts InputGroup, InputGroupAddon, InputGroupText, InputGroupInput, InputGroupTextarea, and InputGroupButton. The classes use Folio tokens.