- Accordion
- Alert
- Alert Dialog
- Aspect Ratio
- Avatar
- Badge
- Breadcrumb
- Button
- Button Group
- Calendar
- Card
- Carousel
- Chart
- Checkbox
- Collapsible
- Combobox
- Command
- Context Menu
- Data Table
- Date Picker
- Dialog
- Drawer
- Dropdown Menu
- Empty
- Field
- Hover Card
- Input
- Input Group
- Input OTP
- Item
- Kbd
- Label
- Menubar
- Native Select
- Navigation Menu
- Pagination
- Popover
- Progress
- Radio Group
- Resizable
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Sonner
- Spinner
- Switch
- Table
- Tabs
- Textarea
- Toast
- Toggle
- Toggle Group
- Tooltip
- Typography
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/components/ui/input-group"
import { Search } from "lucide-react"
export function InputGroupDemo() {
return (
<InputGroup className="max-w-xs">
<InputGroupInput placeholder="Search..." />
<InputGroupAddon>
<Search />
</InputGroupAddon>
<InputGroupAddon align="inline-end">12 results</InputGroupAddon>
</InputGroup>
)
}
Installation
pnpm dlx shadcn@latest add input-group
Usage
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from "@/components/ui/input-group"<InputGroup>
<InputGroupInput placeholder="Search..." />
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
</InputGroup>Align
Use the align prop on InputGroupAddon to position the addon relative to the input.
For proper focus management, InputGroupAddon should always be placed after
InputGroupInput or InputGroupTextarea in the DOM. Use the align prop to
visually position the addon.
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 { Field, FieldDescription, FieldLabel } from "@/components/ui/field"
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/components/ui/input-group"
import { SearchIcon } from "lucide-react"
export function InputGroupInlineStart() {
return (
<Field className="max-w-sm">
<FieldLabel htmlFor="inline-start-input">Input</FieldLabel>
<InputGroup>
<InputGroupInput id="inline-start-input" placeholder="Search..." />
<InputGroupAddon align="inline-start">
<SearchIcon className="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 { Field, FieldDescription, FieldLabel } from "@/components/ui/field"
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/components/ui/input-group"
import { EyeOffIcon } from "lucide-react"
export function InputGroupInlineEnd() {
return (
<Field className="max-w-sm">
<FieldLabel htmlFor="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 {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
} from "@/components/ui/field"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from "@/components/ui/input-group"
import { CopyIcon, FileCodeIcon } from "lucide-react"
export function InputGroupBlockStart() {
return (
<FieldGroup className="max-w-sm">
<Field>
<FieldLabel htmlFor="block-start-input">Input</FieldLabel>
<InputGroup className="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 htmlFor="block-start-textarea">Textarea</FieldLabel>
<InputGroup>
<InputGroupTextarea
id="block-start-textarea"
placeholder="console.log('Hello, world!');"
className="font-mono text-sm"
/>
<InputGroupAddon align="block-start">
<FileCodeIcon className="text-muted-foreground" />
<InputGroupText className="font-mono">script.js</InputGroupText>
<InputGroupButton size="icon-xs" className="ml-auto">
<CopyIcon />
<span className="sr-only">Copy</span>
</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 function InputGroupBlockEnd() {
return (
<FieldGroup className="max-w-sm">
<Field>
<FieldLabel htmlFor="block-end-input">Input</FieldLabel>
<InputGroup className="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 htmlFor="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" className="ml-auto">
Post
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<FieldDescription>
Footer positioned below the textarea.
</FieldDescription>
</Field>
</FieldGroup>
)
}
Examples
Icon
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/components/ui/input-group"
import {
CheckIcon,
CreditCardIcon,
InfoIcon,
MailIcon,
SearchIcon,
StarIcon,
} from "lucide-react"
export function InputGroupIcon() {
return (
<div className="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 function InputGroupTextExample() {
return (
<div className="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" className="!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 className="text-muted-foreground text-xs">
120 characters left
</InputGroupText>
</InputGroupAddon>
</InputGroup>
</div>
)
}
Button
"use client"
import * as React from "react"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/components/ui/input-group"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import {
IconCheck,
IconCopy,
IconInfoCircle,
IconStar,
} from "@tabler/icons-react"
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
export function InputGroupButtonExample() {
const { copyToClipboard, isCopied } = useCopyToClipboard()
const [isFavorite, setIsFavorite] = React.useState(false)
return (
<div className="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={() => {
copyToClipboard("https://x.com/shadcn")
}}
>
{isCopied ? <IconCheck /> : <IconCopy />}
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<InputGroup className="[--radius:9999px]">
<Popover>
<PopoverTrigger render={<InputGroupAddon><InputGroupButton variant="secondary" size="icon-xs">
<IconInfoCircle />
</InputGroupButton></InputGroupAddon>} />
<PopoverContent
align="start"
className="flex flex-col gap-1 rounded-xl text-sm"
>
<p className="font-medium">Your connection is not secure.</p>
<p>You should not enter any sensitive information on this site.</p>
</PopoverContent>
</Popover>
<InputGroupAddon className="text-muted-foreground pl-1.5">
https://
</InputGroupAddon>
<InputGroupInput id="input-secure-19" />
<InputGroupAddon align="inline-end">
<InputGroupButton
onClick={() => setIsFavorite(!isFavorite)}
size="icon-xs"
>
<IconStar
data-favorite={isFavorite}
className="data-[favorite=true]:fill-blue-600 data-[favorite=true]:stroke-blue-600"
/>
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="Type to search..." />
<InputGroupAddon align="inline-end">
<InputGroupButton variant="secondary">Search</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
)
}
Kbd
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/components/ui/input-group"
import { Kbd } from "@/components/ui/kbd"
import { SearchIcon } from "lucide-react"
export function InputGroupKbd() {
return (
<InputGroup className="max-w-sm">
<InputGroupInput placeholder="Search..." />
<InputGroupAddon>
<SearchIcon className="text-muted-foreground" />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<Kbd>⌘K</Kbd>
</InputGroupAddon>
</InputGroup>
)
}
Dropdown
"use client"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/components/ui/input-group"
import { ChevronDownIcon, MoreHorizontal } from "lucide-react"
export function InputGroupDropdown() {
return (
<div className="grid w-full max-w-sm gap-4">
<InputGroup>
<InputGroupInput placeholder="Enter file name" />
<InputGroupAddon align="inline-end">
<DropdownMenu>
<DropdownMenuTrigger render={<InputGroupButton variant="ghost" aria-label="More" size="icon-xs"><MoreHorizontal /></InputGroupButton>} />
<DropdownMenuContent align="end" sideOffset={8} alignOffset={-4}>
<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>
<DropdownMenuTrigger render={<InputGroupButton variant="ghost" className="!pr-1.5 text-xs">Search In... <ChevronDownIcon className="size-3" /></InputGroupButton>} />
<DropdownMenuContent align="end" sideOffset={8} alignOffset={-4}>
<DropdownMenuGroup>
<DropdownMenuItem>Documentation</DropdownMenuItem>
<DropdownMenuItem>Blog Posts</DropdownMenuItem>
<DropdownMenuItem>Changelog</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</InputGroupAddon>
</InputGroup>
</div>
)
}
Spinner
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
} from "@/components/ui/input-group"
import { Spinner } from "@/components/ui/spinner"
import { LoaderIcon } from "lucide-react"
export function InputGroupSpinner() {
return (
<div className="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>
<LoaderIcon className="animate-spin" />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<InputGroupText className="text-muted-foreground">
Please wait...
</InputGroupText>
</InputGroupAddon>
</InputGroup>
</div>
)
}
Textarea
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupTextarea,
} from "@/components/ui/input-group"
import {
IconBrandJavascript,
IconCopy,
IconCornerDownLeft,
IconRefresh,
} from "@tabler/icons-react"
export function InputGroupTextareaExample() {
return (
<div className="grid w-full max-w-md gap-4">
<InputGroup>
<InputGroupTextarea
id="textarea-code-32"
placeholder="console.log('Hello, world!');"
className="min-h-[200px]"
/>
<InputGroupAddon align="block-end" className="border-t">
<InputGroupText>Line 1, Column 1</InputGroupText>
<InputGroupButton size="sm" className="ml-auto" variant="default">
Run <IconCornerDownLeft />
</InputGroupButton>
</InputGroupAddon>
<InputGroupAddon align="block-start" className="border-b">
<InputGroupText className="font-mono font-medium">
<IconBrandJavascript />
script.js
</InputGroupText>
<InputGroupButton className="ml-auto" size="icon-xs">
<IconRefresh />
</InputGroupButton>
<InputGroupButton variant="ghost" size="icon-xs">
<IconCopy />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
)
}
Custom Input
Add the data-slot="input-group-control" attribute to your custom input for automatic focus state handling.
Here's an example of a custom resizable textarea from a third-party library.
"use client"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
} from "@/components/ui/input-group"
import TextareaAutosize from "react-textarea-autosize"
export function InputGroupCustom() {
return (
<div className="grid w-full max-w-sm gap-6">
<InputGroup>
<TextareaAutosize
data-slot="input-group-control"
className="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..."
/>
<InputGroupAddon align="block-end">
<InputGroupButton className="ml-auto" size="sm" variant="default">
Submit
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
)
}
API Reference
InputGroup
The main component that wraps inputs and addons.
| Prop | Type | Default |
|---|---|---|
className | string |
<InputGroup>
<InputGroupInput />
<InputGroupAddon />
</InputGroup>InputGroupAddon
Displays icons, text, buttons, or other content alongside inputs.
For proper focus navigation, the InputGroupAddon component should be placed
after the input. Set the align prop to position the addon.
| Prop | Type | Default |
|---|---|---|
align | "inline-start" | "inline-end" | "block-start" | "block-end" | "inline-start" |
className | string |
<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.
The InputGroupAddon component can have multiple InputGroupButton components and icons.
<InputGroupAddon>
<InputGroupButton>Button</InputGroupButton>
<InputGroupButton>Button</InputGroupButton>
</InputGroupAddon>InputGroupButton
Displays buttons within input groups.
| Prop | Type | Default |
|---|---|---|
size | "xs" | "icon-xs" | "sm" | "icon-sm" | "xs" |
variant | "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | "ghost" |
className | string |
<InputGroupButton>Button</InputGroupButton>
<InputGroupButton size="icon-xs" aria-label="Copy">
<CopyIcon />
</InputGroupButton>InputGroupInput
Replacement for <Input /> when building input groups. This component has the input group styles pre-applied and uses the unified data-slot="input-group-control" for focus state handling.
| Prop | Type | Default |
|---|---|---|
className | string |
All other props are passed through to the underlying <Input /> component.
<InputGroup>
<InputGroupInput placeholder="Enter text..." />
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
</InputGroup>InputGroupTextarea
Replacement for <Textarea /> when building input groups. This component has the textarea group styles pre-applied and uses the unified data-slot="input-group-control" for focus state handling.
| Prop | Type | Default |
|---|---|---|
className | string |
All other props are passed through to the underlying <Textarea /> component.
<InputGroup>
<InputGroupTextarea placeholder="Enter message..." />
<InputGroupAddon align="block-end">
<InputGroupButton>Send</InputGroupButton>
</InputGroupAddon>
</InputGroup>