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><InputGroup>
<InputGroupInput placeholder="workspace" />
<InputGroupAddon>
<InputGroupText>https://</InputGroupText>
</InputGroupAddon>
</InputGroup><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
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 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.
<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>
);
}