Input Group
Add addons, buttons, and helper content to inputs.
Input Group is a native Solid composition built from standard controls and Zaidan wrappers.
import { SearchIcon } from "lucide-solid";import { InputGroup, InputGroupAddon, InputGroupInput } from "~/components/ui/input-group";
export default function InputGroupDemo() { return ( <InputGroup class="max-w-xs"> <InputGroupInput placeholder="Search..." /> <InputGroupAddon> <SearchIcon /> </InputGroupAddon> <InputGroupAddon align="inline-end">12 results</InputGroupAddon> </InputGroup> );}Installation
Usage
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText, InputGroupTextarea,} from "~/components/ui/input-group";<InputGroup> <InputGroupInput placeholder="Search..." /> <InputGroupAddon> <SearchIcon /> </InputGroupAddon></InputGroup>Composition
Use the following composition to build an InputGroup:
InputGroup├── InputGroupInput or InputGroupTextarea├── InputGroupAddon├── InputGroupButton└── InputGroupTextAlign
Use the align prop on InputGroupAddon to position the addon relative to the input.
inline-start
Use align="inline-start" to position the addon at the start of the input. This is the default.
Icon positioned at the start.
import { SearchIcon } from "lucide-solid";import { Field, FieldDescription, FieldLabel } from "~/components/ui/field";import { InputGroup, InputGroupAddon, InputGroupInput } from "~/components/ui/input-group";
export default function InputGroupInlineStart() { return ( <Field class="max-w-sm"> <FieldLabel for="inline-start-input">Input</FieldLabel> <InputGroup> <InputGroupInput id="inline-start-input" placeholder="Search..." /> <InputGroupAddon align="inline-start"> <SearchIcon class="text-muted-foreground" /> </InputGroupAddon> </InputGroup> <FieldDescription>Icon positioned at the start.</FieldDescription> </Field> );}inline-end
Use align="inline-end" to position the addon at the end of the input.
Icon positioned at the end.
import { EyeOffIcon } from "lucide-solid";import { Field, FieldDescription, FieldLabel } from "~/components/ui/field";import { InputGroup, InputGroupAddon, InputGroupInput } from "~/components/ui/input-group";
export default function InputGroupInlineEnd() { return ( <Field class="max-w-sm"> <FieldLabel for="inline-end-input">Input</FieldLabel> <InputGroup> <InputGroupInput id="inline-end-input" type="password" placeholder="Enter password" /> <InputGroupAddon align="inline-end"> <EyeOffIcon /> </InputGroupAddon> </InputGroup> <FieldDescription>Icon positioned at the end.</FieldDescription> </Field> );}block-start
Use align="block-start" to position the addon above the input.
Header positioned above the input.
Header positioned above the textarea.
import { CopyIcon, FileCodeIcon } from "lucide-solid";import { Field, FieldDescription, FieldGroup, FieldLabel } from "~/components/ui/field";import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText, InputGroupTextarea,} from "~/components/ui/input-group";
export default function InputGroupBlockStart() { return ( <FieldGroup class="max-w-sm"> <Field> <FieldLabel for="block-start-input">Input</FieldLabel> <InputGroup class="h-auto"> <InputGroupInput id="block-start-input" placeholder="Enter your name" /> <InputGroupAddon align="block-start"> <InputGroupText>Full Name</InputGroupText> </InputGroupAddon> </InputGroup> <FieldDescription>Header positioned above the input.</FieldDescription> </Field> <Field> <FieldLabel for="block-start-textarea">Textarea</FieldLabel> <InputGroup> <InputGroupTextarea id="block-start-textarea" placeholder="console.log('Hello, world!');" class="font-mono text-sm" /> <InputGroupAddon align="block-start"> <FileCodeIcon class="text-muted-foreground" /> <InputGroupText class="font-mono">script.js</InputGroupText> <InputGroupButton size="icon-xs" class="ml-auto" aria-label="Copy"> <CopyIcon /> </InputGroupButton> </InputGroupAddon> </InputGroup> <FieldDescription>Header positioned above the textarea.</FieldDescription> </Field> </FieldGroup> );}block-end
Use align="block-end" to position the addon below the input.
Footer positioned below the input.
Footer positioned below the textarea.
import { Field, FieldDescription, FieldGroup, FieldLabel } from "~/components/ui/field";import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText, InputGroupTextarea,} from "~/components/ui/input-group";
export default function InputGroupBlockEnd() { return ( <FieldGroup class="max-w-sm"> <Field> <FieldLabel for="block-end-input">Input</FieldLabel> <InputGroup class="h-auto"> <InputGroupInput id="block-end-input" placeholder="Enter amount" /> <InputGroupAddon align="block-end"> <InputGroupText>USD</InputGroupText> </InputGroupAddon> </InputGroup> <FieldDescription>Footer positioned below the input.</FieldDescription> </Field> <Field> <FieldLabel for="block-end-textarea">Textarea</FieldLabel> <InputGroup> <InputGroupTextarea id="block-end-textarea" placeholder="Write a comment..." /> <InputGroupAddon align="block-end"> <InputGroupText>0/280</InputGroupText> <InputGroupButton variant="default" size="sm" class="ml-auto"> Post </InputGroupButton> </InputGroupAddon> </InputGroup> <FieldDescription>Footer positioned below the textarea.</FieldDescription> </Field> </FieldGroup> );}Icon
import { CheckIcon, CreditCardIcon, InfoIcon, MailIcon, SearchIcon, StarIcon } from "lucide-solid";import { InputGroup, InputGroupAddon, InputGroupInput } from "~/components/ui/input-group";
export default function InputGroupIcon() { return ( <div class="grid w-full max-w-sm gap-6"> <InputGroup> <InputGroupInput placeholder="Search..." /> <InputGroupAddon> <SearchIcon /> </InputGroupAddon> </InputGroup> <InputGroup> <InputGroupInput type="email" placeholder="Enter your email" /> <InputGroupAddon> <MailIcon /> </InputGroupAddon> </InputGroup> <InputGroup> <InputGroupInput placeholder="Card number" /> <InputGroupAddon> <CreditCardIcon /> </InputGroupAddon> <InputGroupAddon align="inline-end"> <CheckIcon /> </InputGroupAddon> </InputGroup> <InputGroup> <InputGroupInput placeholder="Card number" /> <InputGroupAddon align="inline-end"> <StarIcon /> <InfoIcon /> </InputGroupAddon> </InputGroup> </div> );}Text
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText, InputGroupTextarea,} from "~/components/ui/input-group";
export default function InputGroupTextExample() { return ( <div class="grid w-full max-w-sm gap-6"> <InputGroup> <InputGroupAddon> <InputGroupText>$</InputGroupText> </InputGroupAddon> <InputGroupInput placeholder="0.00" /> <InputGroupAddon align="inline-end"> <InputGroupText>USD</InputGroupText> </InputGroupAddon> </InputGroup> <InputGroup> <InputGroupAddon> <InputGroupText>https://</InputGroupText> </InputGroupAddon> <InputGroupInput placeholder="example.com" class="pl-0.5!" /> <InputGroupAddon align="inline-end"> <InputGroupText>.com</InputGroupText> </InputGroupAddon> </InputGroup> <InputGroup> <InputGroupInput placeholder="Enter your username" /> <InputGroupAddon align="inline-end"> <InputGroupText>@company.com</InputGroupText> </InputGroupAddon> </InputGroup> <InputGroup> <InputGroupTextarea placeholder="Enter your message" /> <InputGroupAddon align="block-end"> <InputGroupText class="text-xs text-muted-foreground">120 characters left</InputGroupText> </InputGroupAddon> </InputGroup> </div> );}Button
import { CheckIcon, CopyIcon, InfoIcon, StarIcon } from "lucide-solid";import { createSignal } from "solid-js";import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput,} from "~/components/ui/input-group";import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
export default function InputGroupButtonExample() { const [copied, setCopied] = createSignal(false); const [favorite, setFavorite] = createSignal(false);
const copy = async () => { await navigator.clipboard?.writeText("https://x.com/shadcn"); setCopied(true); };
return ( <div class="grid w-full max-w-sm gap-6"> <InputGroup> <InputGroupInput placeholder="https://x.com/shadcn" readonly /> <InputGroupAddon align="inline-end"> <InputGroupButton aria-label="Copy" title="Copy" size="icon-xs" onClick={copy}> {copied() ? <CheckIcon /> : <CopyIcon />} </InputGroupButton> </InputGroupAddon> </InputGroup> <InputGroup class="[--radius:9999px]"> <Popover placement="bottom-start"> <PopoverTrigger as={InputGroupAddon}> <InputGroupButton variant="secondary" size="icon-xs" aria-label="Connection information" > <InfoIcon /> </InputGroupButton> </PopoverTrigger> <PopoverContent class="flex flex-col gap-1 rounded-xl text-sm"> <p class="font-medium">Your connection is not secure.</p> <p>You should not enter any sensitive information on this site.</p> </PopoverContent> </Popover> <InputGroupAddon class="pl-1.5 text-muted-foreground">https://</InputGroupAddon> <InputGroupInput id="input-secure-19" /> <InputGroupAddon align="inline-end"> <InputGroupButton onClick={() => setFavorite((value) => !value)} size="icon-xs" aria-label="Toggle favorite" aria-pressed={favorite()} > <StarIcon class="data-[favorite=true]:fill-blue-600 data-[favorite=true]:stroke-blue-600" data-favorite={favorite()} /> </InputGroupButton> </InputGroupAddon> </InputGroup> <InputGroup> <InputGroupInput placeholder="Type to search..." /> <InputGroupAddon align="inline-end"> <InputGroupButton variant="secondary">Search</InputGroupButton> </InputGroupAddon> </InputGroup> </div> );}Kbd
import { SearchIcon } from "lucide-solid";import { InputGroup, InputGroupAddon, InputGroupInput } from "~/components/ui/input-group";import { Kbd } from "~/components/ui/kbd";
export default function InputGroupKbd() { return ( <InputGroup class="max-w-sm"> <InputGroupInput placeholder="Search..." /> <InputGroupAddon> <SearchIcon class="text-muted-foreground" /> </InputGroupAddon> <InputGroupAddon align="inline-end"> <Kbd>⌘K</Kbd> </InputGroupAddon> </InputGroup> );}Dropdown
import { ChevronDownIcon, MoreHorizontalIcon } from "lucide-solid";import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuTrigger,} from "~/components/ui/dropdown-menu";import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput,} from "~/components/ui/input-group";
export default function InputGroupDropdown() { return ( <div class="grid w-full max-w-sm gap-4"> <InputGroup> <InputGroupInput placeholder="Enter file name" /> <InputGroupAddon align="inline-end"> <DropdownMenu placement="bottom-end" gutter={8}> <DropdownMenuTrigger as={InputGroupButton} variant="ghost" aria-label="More" size="icon-xs" > <MoreHorizontalIcon /> </DropdownMenuTrigger> <DropdownMenuContent> <DropdownMenuGroup> <DropdownMenuItem>Settings</DropdownMenuItem> <DropdownMenuItem>Copy path</DropdownMenuItem> <DropdownMenuItem>Open location</DropdownMenuItem> </DropdownMenuGroup> </DropdownMenuContent> </DropdownMenu> </InputGroupAddon> </InputGroup> <InputGroup> <InputGroupInput placeholder="Enter search query" /> <InputGroupAddon align="inline-end"> <DropdownMenu placement="bottom-end" gutter={8}> <DropdownMenuTrigger as={InputGroupButton} variant="ghost" class="pr-1.5! text-xs"> Search In... <ChevronDownIcon class="size-3" /> </DropdownMenuTrigger> <DropdownMenuContent> <DropdownMenuGroup> <DropdownMenuItem>Documentation</DropdownMenuItem> <DropdownMenuItem>Blog Posts</DropdownMenuItem> <DropdownMenuItem>Changelog</DropdownMenuItem> </DropdownMenuGroup> </DropdownMenuContent> </DropdownMenu> </InputGroupAddon> </InputGroup> </div> );}Spinner
import { LoaderCircleIcon } from "lucide-solid";import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText,} from "~/components/ui/input-group";import { Spinner } from "~/components/ui/spinner";
export default function InputGroupSpinner() { return ( <div class="grid w-full max-w-sm gap-4"> <InputGroup> <InputGroupInput placeholder="Searching..." /> <InputGroupAddon align="inline-end"> <Spinner /> </InputGroupAddon> </InputGroup> <InputGroup> <InputGroupInput placeholder="Processing..." /> <InputGroupAddon> <Spinner /> </InputGroupAddon> </InputGroup> <InputGroup> <InputGroupInput placeholder="Saving changes..." /> <InputGroupAddon align="inline-end"> <InputGroupText>Saving...</InputGroupText> <Spinner /> </InputGroupAddon> </InputGroup> <InputGroup> <InputGroupInput placeholder="Refreshing data..." /> <InputGroupAddon> <LoaderCircleIcon class="animate-spin" /> </InputGroupAddon> <InputGroupAddon align="inline-end"> <InputGroupText class="text-muted-foreground">Please wait...</InputGroupText> </InputGroupAddon> </InputGroup> </div> );}Textarea
import { BracesIcon, CopyIcon, CornerDownLeftIcon, RefreshCwIcon } from "lucide-solid";import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupTextarea,} from "~/components/ui/input-group";
export default function InputGroupTextareaExample() { return ( <div class="grid w-full max-w-md gap-4"> <InputGroup> <InputGroupTextarea id="textarea-code-32" placeholder="console.log('Hello, world!');" class="min-h-[200px]" /> <InputGroupAddon align="block-end" class="border-t"> <InputGroupText>Line 1, Column 1</InputGroupText> <InputGroupButton size="sm" class="ml-auto" variant="default"> Run <CornerDownLeftIcon /> </InputGroupButton> </InputGroupAddon> <InputGroupAddon align="block-start" class="border-b"> <InputGroupText class="font-mono font-medium"> <BracesIcon /> script.js </InputGroupText> <InputGroupButton class="ml-auto" size="icon-xs" aria-label="Refresh"> <RefreshCwIcon /> </InputGroupButton> <InputGroupButton variant="ghost" size="icon-xs" aria-label="Copy"> <CopyIcon /> </InputGroupButton> </InputGroupAddon> </InputGroup> </div> );}Custom Input
Add the data-slot="input-group-control" attribute to a custom input for automatic focus state handling.
import { InputGroup, InputGroupAddon, InputGroupButton } from "~/components/ui/input-group";
export default function InputGroupCustom() { const resize = (event: InputEvent & { currentTarget: HTMLTextAreaElement }) => { const textarea = event.currentTarget; textarea.style.height = "auto"; textarea.style.height = `${textarea.scrollHeight}px`; };
return ( <div class="grid w-full max-w-sm gap-6"> <InputGroup> <textarea data-slot="input-group-control" class="flex field-sizing-content min-h-16 w-full resize-none rounded-md bg-transparent px-3 py-2.5 text-base transition-[color,box-shadow] outline-none md:text-sm" placeholder="Autoresize textarea..." onInput={resize} /> <InputGroupAddon align="block-end"> <InputGroupButton class="ml-auto" size="sm" variant="default"> Submit </InputGroupButton> </InputGroupAddon> </InputGroup> </div> );}API Reference
InputGroup
The main component that wraps inputs and addons.
<InputGroup> <InputGroupInput /> <InputGroupAddon /></InputGroup>InputGroupAddon
Displays icons, text, buttons, or other content alongside inputs. Place the addon after its input in the DOM and use align to position it visually.
<InputGroupAddon align="inline-end"> <SearchIcon /></InputGroupAddon>For <InputGroupInput />, use the inline-start or inline-end alignment. For <InputGroupTextarea />, use the block-start or block-end alignment.
InputGroupButton
Displays buttons within input groups.
<InputGroupButton>Button</InputGroupButton>InputGroupInput
Replacement for <Input /> when building input groups. It applies input-group styles and data-slot="input-group-control" for focus state handling.
InputGroupTextarea
Replacement for <Textarea /> when building input groups. It applies textarea-group styles and data-slot="input-group-control" for focus state handling.