Pair an input or textarea with icons, text, buttons, and helper content.
import { InputGroup, InputGroupAddon, InputGroupInput,} from "@/components/ui/input-group"function SearchIcon(props: React.SVGProps<SVGSVGElement>) { return ( <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}> <circle cx="11" cy="11" r="8" /> <path d="m21 21-4.3-4.3" /> </svg> )}export function Example() { return ( <InputGroup className="max-w-xs" variant="filled"> <InputGroupInput placeholder="Search..." /> <InputGroupAddon> <SearchIcon /> </InputGroupAddon> <InputGroupAddon align="inline-end">12 results</InputGroupAddon> </InputGroup> )}Installation
npx @arctis-sh/@arctis-sh/ui@latest add input-groupUsage
import { InputGroup, InputGroupAddon, InputGroupInput,} from "@/components/ui/input-group"<InputGroup> <InputGroupInput placeholder="Search…" /> <InputGroupAddon> <SearchIcon /> </InputGroupAddon></InputGroup>Composition
InputGroup ├── InputGroupInput or InputGroupTextarea ├── InputGroupAddon ├── InputGroupButton └── InputGroupText
Align
Set align on InputGroupAddon to place the addon. Keep addons after the control in the DOM; align controls their visual position.
inline-start
The default. Places the addon at the start of the input.
import { InputGroup, InputGroupAddon, InputGroupInput,} from "@/components/ui/input-group"function SearchIcon(props: React.SVGProps<SVGSVGElement>) { return ( <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}> <circle cx="11" cy="11" r="8" /> <path d="m21 21-4.3-4.3" /> </svg> )}export function Example() { return ( <InputGroup className="max-w-xs"> <InputGroupInput placeholder="Search..." /> <InputGroupAddon align="inline-start"> <SearchIcon /> </InputGroupAddon> </InputGroup> )}inline-end
Addon at the end of the input.
import { InputGroup, InputGroupAddon, InputGroupInput,} from "@/components/ui/input-group"function EyeOffIcon(props: React.SVGProps<SVGSVGElement>) { return ( <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}> <path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" /> <path d="M14.084 14.158a3 3 0 0 1-4.242-4.242" /> <path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" /> <path d="m2 2 20 20" /> </svg> )}export function Example() { return ( <InputGroup className="max-w-xs"> <InputGroupInput type="password" placeholder="Password" /> <InputGroupAddon align="inline-end"> <EyeOffIcon /> </InputGroupAddon> </InputGroup> )}block-start
Addon above the control.
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText, InputGroupTextarea,} from "@/components/ui/input-group"function CopyIcon(props: React.SVGProps<SVGSVGElement>) { return ( <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}> <rect x="9" y="9" width="13" height="13" rx="2" ry="2" /> <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /> </svg> )}function FileCodeIcon(props: React.SVGProps<SVGSVGElement>) { return ( <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}> <path d="M4 22h14a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v4" /> <path d="M14 2v4a2 2 0 0 0 2 2h4" /> <path d="m5 12-3 3 3 3" /> <path d="m9 18 3-3-3-3" /> </svg> )}export function Example() { return ( <div className="flex w-full max-w-sm flex-col gap-4"> <InputGroup> <InputGroupInput placeholder="Enter your name" /> <InputGroupAddon align="block-start"> <InputGroupText>Full Name</InputGroupText> </InputGroupAddon> </InputGroup> <InputGroup variant="filled"> <InputGroupTextarea placeholder="// write a function..." className="min-h-24" /> <InputGroupAddon align="block-start" className="border-b border-border"> <InputGroupText> <FileCodeIcon /> script.js </InputGroupText> <InputGroupButton size="icon-xs" className="ml-auto" aria-label="Copy"> <CopyIcon /> </InputGroupButton> </InputGroupAddon> </InputGroup> </div> )}block-end
Addon below the control.
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText, InputGroupTextarea,} from "@/components/ui/input-group"export function Example() { return ( <div className="flex w-full max-w-sm flex-col gap-4"> <InputGroup> <InputGroupInput placeholder="0.00" /> <InputGroupAddon align="block-end"> <InputGroupText>USD</InputGroupText> </InputGroupAddon> </InputGroup> <InputGroup variant="filled"> <InputGroupTextarea placeholder="Write a comment..." className="min-h-24" /> <InputGroupAddon align="block-end" className="border-t border-border"> <InputGroupText>0/280</InputGroupText> <InputGroupButton className="ml-auto" size="sm" variant="default"> Post </InputGroupButton> </InputGroupAddon> </InputGroup> </div> )}Icon
import { InputGroup, InputGroupAddon, InputGroupInput,} from "@/components/ui/input-group"function CheckIcon(props: React.SVGProps<SVGSVGElement>) { return ( <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}> <path d="M20 6 9 17l-5-5" /> </svg> )}function CreditCardIcon(props: React.SVGProps<SVGSVGElement>) { return ( <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}> <rect width="20" height="14" x="2" y="5" rx="2" /> <path d="M2 10h20" /> </svg> )}function MailIcon(props: React.SVGProps<SVGSVGElement>) { return ( <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}> <path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7" /> <rect x="2" y="4" width="20" height="16" rx="2" /> </svg> )}export function Example() { return ( <div className="grid w-full max-w-sm gap-4"> <InputGroup> <InputGroupInput placeholder="Email" /> <InputGroupAddon> <MailIcon /> </InputGroupAddon> </InputGroup> <InputGroup variant="filled"> <InputGroupInput placeholder="Card number" /> <InputGroupAddon> <CreditCardIcon /> </InputGroupAddon> <InputGroupAddon align="inline-end"> <CheckIcon /> </InputGroupAddon> </InputGroup> </div> )}Text
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText,} from "@/components/ui/input-group"export function Example() { return ( <div className="grid w-full max-w-sm gap-4"> <InputGroup> <InputGroupInput placeholder="0.00" /> <InputGroupAddon> <InputGroupText>$</InputGroupText> </InputGroupAddon> <InputGroupAddon align="inline-end"> <InputGroupText>USD</InputGroupText> </InputGroupAddon> </InputGroup> <InputGroup variant="filled"> <InputGroupInput placeholder="example" /> <InputGroupAddon> <InputGroupText>https://</InputGroupText> </InputGroupAddon> <InputGroupAddon align="inline-end"> <InputGroupText>.com</InputGroupText> </InputGroupAddon> </InputGroup> <InputGroup variant="filled"> <InputGroupInput placeholder="username" /> <InputGroupAddon align="inline-end"> <InputGroupText>@company.com</InputGroupText> </InputGroupAddon> </InputGroup> </div> )}Button
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText,} from "@/components/ui/input-group"function SearchIcon(props: React.SVGProps<SVGSVGElement>) { return ( <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}> <circle cx="11" cy="11" r="8" /> <path d="m21 21-4.3-4.3" /> </svg> )}export function Example() { return ( <div className="grid w-full max-w-sm gap-4"> <InputGroup> <InputGroupInput placeholder="example.com" /> <InputGroupAddon> <InputGroupText>https://</InputGroupText> </InputGroupAddon> <InputGroupAddon align="inline-end"> <InputGroupButton variant="default">Search</InputGroupButton> </InputGroupAddon> </InputGroup> <InputGroup variant="filled"> <InputGroupInput placeholder="Find anything..." /> <InputGroupAddon align="inline-end"> <InputGroupButton variant="secondary">Go</InputGroupButton> </InputGroupAddon> </InputGroup> <InputGroup> <InputGroupInput placeholder="Search..." /> <InputGroupAddon align="inline-end"> <InputGroupButton size="icon-xs" variant="outline" aria-label="Search"> <SearchIcon /> </InputGroupButton> </InputGroupAddon> </InputGroup> <InputGroup variant="filled"> <InputGroupInput placeholder="Filter..." /> <InputGroupAddon align="inline-end"> <InputGroupButton size="icon-xs" aria-label="Search"> <SearchIcon /> </InputGroupButton> </InputGroupAddon> </InputGroup> </div> )}Variant
variant="filled" uses a muted fill without an outline. The default variant keeps the border.
<InputGroup variant="filled"> <InputGroupInput placeholder="Search…" /></InputGroup>Kbd
import { Kbd } from "@/components/ui/kbd"import { InputGroup, InputGroupAddon, InputGroupInput,} from "@/components/ui/input-group"function SearchIcon(props: React.SVGProps<SVGSVGElement>) { return ( <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}> <circle cx="11" cy="11" r="8" /> <path d="m21 21-4.3-4.3" /> </svg> )}export function Example() { return ( <InputGroup className="max-w-xs"> <InputGroupInput placeholder="Search..." /> <InputGroupAddon> <SearchIcon /> </InputGroupAddon> <InputGroupAddon align="inline-end"> <Kbd>⌘K</Kbd> </InputGroupAddon> </InputGroup> )}Spinner
import { Spinner } from "@/components/ui/spinner"import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText,} from "@/components/ui/input-group"export function Example() { return ( <div className="grid w-full max-w-sm gap-4"> <InputGroup> <InputGroupInput placeholder="Saving..." disabled /> <InputGroupAddon align="inline-end"> <Spinner /> </InputGroupAddon> </InputGroup> <InputGroup data-disabled> <InputGroupInput placeholder="Please wait..." disabled /> <InputGroupAddon> <Spinner /> </InputGroupAddon> <InputGroupAddon align="inline-end"> <InputGroupText>Saving...</InputGroupText> </InputGroupAddon> </InputGroup> </div> )}Textarea
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupTextarea,} from "@/components/ui/input-group"function CopyIcon(props: React.SVGProps<SVGSVGElement>) { return ( <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}> <rect x="9" y="9" width="13" height="13" rx="2" ry="2" /> <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /> </svg> )}function RefreshIcon(props: React.SVGProps<SVGSVGElement>) { return ( <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}> <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /> <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" /> <path d="M8 16H3v5" /> </svg> )}export function Example() { return ( <div className="grid w-full max-w-md gap-4"> <InputGroup variant="filled"> <InputGroupTextarea placeholder="Write a comment..." className="min-h-24" /> <InputGroupAddon align="block-end" className="border-t border-border"> <InputGroupText>Line 1, Column 1</InputGroupText> <InputGroupButton className="ml-auto" size="sm" variant="default"> Run </InputGroupButton> </InputGroupAddon> </InputGroup> <InputGroup variant="filled"> <InputGroupTextarea defaultValue="console.log('hello')" className="min-h-24" /> <InputGroupAddon align="block-start" className="border-b border-border"> <InputGroupText>script.js</InputGroupText> <InputGroupButton size="icon-xs" className="ml-auto" aria-label="Refresh"> <RefreshIcon /> </InputGroupButton> <InputGroupButton size="icon-xs" aria-label="Copy"> <CopyIcon /> </InputGroupButton> </InputGroupAddon> </InputGroup> </div> )}API Reference
InputGroup
Groups the control with its addons.
| Prop | Type | Default |
|---|---|---|
| variant | "default" | "filled" | "default" |
| className | string | — |
<InputGroup> <InputGroupInput /> <InputGroupAddon /></InputGroup><InputGroup variant="filled"> <InputGroupInput placeholder="Search…" /></InputGroup>InputGroupAddon
Holds icons, text, or buttons beside the control. Place it after the input in the DOM, then use align to set its visual position.
| Prop | Type | Default |
|---|---|---|
| align | "inline-start" | "inline-end" | "block-start" | "block-end" | "inline-start" |
| className | string | — |
<InputGroupAddon align="inline-end"> <SearchIcon /></InputGroupAddon>For InputGroupInput, prefer inline-start or inline-end. For InputGroupTextarea, prefer block-start or block-end.
<InputGroupAddon> <InputGroupButton>Button</InputGroupButton> <InputGroupButton>Button</InputGroupButton></InputGroupAddon>InputGroupButton
Buttons inside an addon.
| Prop | Type | Default |
|---|---|---|
| size | "xs" | "sm" | "icon-xs" | "icon-sm" | "xs" |
| variant | Button variant | "ghost" |
| className | string | — |
<InputGroupButton>Button</InputGroupButton><InputGroupButton size="icon-xs" aria-label="Copy"> <CopyIcon /></InputGroupButton>InputGroupInput
Input styled to sit inside a group. It uses data-slot="input-group-control" and passes other props to Input.
| Prop | Type | Default |
|---|---|---|
| className | string | — |
<InputGroup> <InputGroupInput placeholder="Enter text..." /> <InputGroupAddon> <SearchIcon /> </InputGroupAddon></InputGroup>InputGroupTextarea
Textarea styled to sit inside a group. It uses data-slot="input-group-control" and passes other props to Textarea.
| Prop | Type | Default |
|---|---|---|
| className | string | — |
<InputGroup> <InputGroupTextarea placeholder="Enter message..." /> <InputGroupAddon align="block-end"> <InputGroupButton>Send</InputGroupButton> </InputGroupAddon></InputGroup>InputGroupText
Muted helper text inside an addon.
<InputGroupText>https://</InputGroupText>