Github

Input Group

Add addons, buttons, and helper content to inputs.

12 results
import { Search } from "lucide-react";

import {
	InputGroup,
	InputGroupAddon,
	InputGroupInput,
} from "@/components/ui/input-group";

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 https://herocn.dev/r/input-group.json

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
└── InputGroupText

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 { SearchIcon } from "lucide-react";

import {
	Field,
	FieldDescription,
	FieldLabel,
} from "@/components/ui/field";
import {
	InputGroup,
	InputGroupAddon,
	InputGroupInput,
} from "@/components/ui/input-group";

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 { EyeOffIcon } from "lucide-react";

import {
	Field,
	FieldDescription,
	FieldLabel,
} from "@/components/ui/field";
import {
	InputGroup,
	InputGroupAddon,
	InputGroupInput,
} from "@/components/ui/input-group";

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.

Full Name

Header positioned above the input.

script.js

Header positioned above the textarea.

import { CopyIcon, FileCodeIcon } from "lucide-react";

import {
	Field,
	FieldDescription,
	FieldGroup,
	FieldLabel,
} from "@/components/ui/field";
import {
	InputGroup,
	InputGroupAddon,
	InputGroupButton,
	InputGroupInput,
	InputGroupText,
	InputGroupTextarea,
} from "@/components/ui/input-group";

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.

USD

Footer positioned below the input.

0/280

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

Variants

import { SearchIcon } from "lucide-react";

import {
	InputGroup,
	InputGroupAddon,
	InputGroupInput,
} from "@/components/ui/input-group";

export function InputGroupVariants() {
	return (
		<div className="grid w-full max-w-sm gap-4">
			<InputGroup>
				<InputGroupInput placeholder="Default" />
				<InputGroupAddon>
					<SearchIcon />
				</InputGroupAddon>
			</InputGroup>
			<InputGroup variant="secondary">
				<InputGroupInput placeholder="Secondary" />
				<InputGroupAddon>
					<SearchIcon />
				</InputGroupAddon>
			</InputGroup>
		</div>
	);
}

Invalid

import { SearchIcon } from "lucide-react";

import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import {
	InputGroup,
	InputGroupAddon,
	InputGroupInput,
	InputGroupTextarea,
} from "@/components/ui/input-group";

export function InputGroupInvalid() {
	return (
		<div className="grid w-full max-w-sm gap-6">
			<Field data-invalid>
				<FieldLabel htmlFor="invalid-input">Search</FieldLabel>
				<InputGroup>
					<InputGroupInput
						id="invalid-input"
						aria-invalid
						placeholder="Search..."
					/>
					<InputGroupAddon>
						<SearchIcon />
					</InputGroupAddon>
				</InputGroup>
				<FieldError>Please enter a valid search term.</FieldError>
			</Field>
			<Field data-invalid>
				<FieldLabel htmlFor="invalid-textarea">Message</FieldLabel>
				<InputGroup>
					<InputGroupTextarea
						id="invalid-textarea"
						aria-invalid
						placeholder="Enter your message..."
					/>
				</InputGroup>
				<FieldError>Message cannot be empty.</FieldError>
			</Field>
		</div>
	);
}

Disabled

import { SearchIcon } from "lucide-react";

import { Field, FieldLabel } from "@/components/ui/field";
import {
	InputGroup,
	InputGroupAddon,
	InputGroupInput,
	InputGroupTextarea,
} from "@/components/ui/input-group";

export function InputGroupDisabled() {
	return (
		<div className="grid w-full max-w-sm gap-6">
			<Field>
				<FieldLabel htmlFor="disabled-input">Search</FieldLabel>
				<InputGroup>
					<InputGroupInput
						id="disabled-input"
						disabled
						placeholder="Search disabled..."
					/>
					<InputGroupAddon>
						<SearchIcon />
					</InputGroupAddon>
				</InputGroup>
			</Field>

			<Field>
				<FieldLabel htmlFor="disabled-textarea">Message</FieldLabel>
				<InputGroup>
					<InputGroupTextarea
						id="disabled-textarea"
						disabled
						placeholder="Message disabled..."
						rows={3}
					/>
				</InputGroup>
			</Field>
		</div>
	);
}

Icon

import {
	CheckIcon,
	CreditCardIcon,
	InfoIcon,
	MailIcon,
	SearchIcon,
	StarIcon,
} from "lucide-react";

import {
	InputGroup,
	InputGroupAddon,
	InputGroupInput,
} from "@/components/ui/input-group";

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

$
USD
https://
.com
@company.com
120 characters left
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 { CheckIcon, CopyIcon, StarIcon } from "lucide-react";
import * as React from "react";

import {
	InputGroup,
	InputGroupAddon,
	InputGroupButton,
	InputGroupInput,
} from "@/components/ui/input-group";

export function InputGroupButtonExample() {
	const [isCopied, setIsCopied] = React.useState(false);
	const [isFavorite, setIsFavorite] = React.useState(false);

	const handleCopy = () => {
		navigator.clipboard.writeText("https://x.com/0xMaqed");
		setIsCopied(true);
		setTimeout(() => setIsCopied(false), 2000);
	};

	return (
		<div className="grid w-full max-w-sm gap-6">
			<InputGroup>
				<InputGroupInput placeholder="https://x.com/0xMaqed" readOnly />
				<InputGroupAddon align="inline-end">
					<InputGroupButton
						aria-label="Copy"
						title="Copy"
						size="icon-sm"
						onClick={handleCopy}
					>
						{isCopied ? <CheckIcon /> : <CopyIcon />}
					</InputGroupButton>
				</InputGroupAddon>
			</InputGroup>
			<InputGroup>
				<InputGroupInput placeholder="Type to search..." />
				<InputGroupAddon align="inline-end">
					<InputGroupButton variant="secondary">Search</InputGroupButton>
				</InputGroupAddon>
			</InputGroup>
			<InputGroup>
				<InputGroupInput placeholder="Add to favorites..." />
				<InputGroupAddon align="inline-end">
					<InputGroupButton
						onClick={() => setIsFavorite(!isFavorite)}
						size="icon-sm"
					>
						<StarIcon
							data-favorite={isFavorite}
							className="data-[favorite=true]:fill-blue-600 data-[favorite=true]:stroke-blue-600"
						/>
					</InputGroupButton>
				</InputGroupAddon>
			</InputGroup>
		</div>
	);
}

Spinner

Spinner SVG
Spinner SVG
Saving...Spinner SVG
Please wait...
import { LoaderIcon } from "lucide-react";

import {
	InputGroup,
	InputGroupAddon,
	InputGroupInput,
	InputGroupText,
} from "@/components/ui/input-group";
import { Spinner } from "@/components/ui/spinner";

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

Line 1, Column 1
script.js
import {
	CodeIcon,
	CopyIcon,
	CornerDownLeftIcon,
	RefreshCwIcon,
} from "lucide-react";

import {
	InputGroup,
	InputGroupAddon,
	InputGroupButton,
	InputGroupText,
	InputGroupTextarea,
} from "@/components/ui/input-group";

export function InputGroupTextareaExample() {
	return (
		<div className="grid w-full max-w-md gap-4">
			<InputGroup>
				<InputGroupTextarea
					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 <CornerDownLeftIcon />
					</InputGroupButton>
				</InputGroupAddon>
				<InputGroupAddon align="block-start" className="border-b">
					<InputGroupText className="font-medium font-mono">
						<CodeIcon />
						script.js
					</InputGroupText>
					<InputGroupButton className="ml-auto" size="icon-sm">
						<RefreshCwIcon />
					</InputGroupButton>
					<InputGroupButton variant="ghost" size="icon-sm">
						<CopyIcon />
					</InputGroupButton>
				</InputGroupAddon>
			</InputGroup>
		</div>
	);
}
"use client";

import { ChevronDownIcon, MoreHorizontalIcon } from "lucide-react";

import {
	DropdownMenu,
	DropdownMenuContent,
	DropdownMenuGroup,
	DropdownMenuItem,
	DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
	InputGroup,
	InputGroupAddon,
	InputGroupButton,
	InputGroupInput,
} from "@/components/ui/input-group";

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-sm"
								/>
							}
						>
							<MoreHorizontalIcon />
						</DropdownMenuTrigger>
						<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" />
						</DropdownMenuTrigger>
						<DropdownMenuContent align="end" sideOffset={8} alignOffset={-4}>
							<DropdownMenuGroup>
								<DropdownMenuItem>Documentation</DropdownMenuItem>
								<DropdownMenuItem>Blog Posts</DropdownMenuItem>
								<DropdownMenuItem>Changelog</DropdownMenuItem>
							</DropdownMenuGroup>
						</DropdownMenuContent>
					</DropdownMenu>
				</InputGroupAddon>
			</InputGroup>
		</div>
	);
}

In surface

Use variant="secondary" when placing an InputGroup inside a card or panel surface for a seamless, shadow-free appearance that blends with the surface background.

Find a member
import { SearchIcon } from "lucide-react";

import {
	Card,
	CardContent,
	CardHeader,
	CardTitle,
} from "@/components/ui/card";
import {
	InputGroup,
	InputGroupAddon,
	InputGroupButton,
	InputGroupInput,
} from "@/components/ui/input-group";

export function InputGroupInSurface() {
	return (
		<Card className="w-full max-w-sm">
			<CardHeader>
				<CardTitle>Find a member</CardTitle>
			</CardHeader>
			<CardContent>
				<InputGroup variant="secondary">
					<InputGroupInput placeholder="Search by name or email..." />
					<InputGroupAddon align="inline-end">
						<InputGroupButton size="icon-sm">
							<SearchIcon />
						</InputGroupButton>
					</InputGroupAddon>
				</InputGroup>
			</CardContent>
		</Card>
	);
}

RTL

١٢ نتيجة
Spinner SVG
جاري الحفظ...Spinner SVG
٠/٢٨٠

تذييل موضع أسفل منطقة النص.

"use client";

import { Search } from "lucide-react";

import {
	type Translations,
	useTranslation,
} from "@/components/language-selector";
import {
	Field,
	FieldDescription,
	FieldGroup,
	FieldLabel,
} from "@/components/ui/field";
import {
	InputGroup,
	InputGroupAddon,
	InputGroupButton,
	InputGroupInput,
	InputGroupText,
	InputGroupTextarea,
} from "@/components/ui/input-group";
import { Spinner } from "@/components/ui/spinner";

const translations: Translations = {
	en: {
		dir: "ltr",
		values: {
			placeholder: "Search...",
			results: "12 results",
			searching: "Searching...",
			saving: "Saving...",
			savingChanges: "Saving changes...",
			textareaLabel: "Textarea",
			textareaPlaceholder: "Write a comment...",
			characterCount: "0/280",
			post: "Post",
			textareaDescription: "Footer positioned below the textarea.",
		},
	},
	ar: {
		dir: "rtl",
		values: {
			placeholder: "بحث...",
			results: "١٢ نتيجة",
			searching: "جاري البحث...",
			saving: "جاري الحفظ...",
			savingChanges: "جاري حفظ التغييرات...",
			textareaLabel: "منطقة النص",
			textareaPlaceholder: "اكتب تعليقًا...",
			characterCount: "٠/٢٨٠",
			post: "نشر",
			textareaDescription: "تذييل موضع أسفل منطقة النص.",
		},
	},
	he: {
		dir: "rtl",
		values: {
			placeholder: "חפש...",
			results: "12 תוצאות",
			searching: "מחפש...",
			saving: "שומר...",
			savingChanges: "שומר שינויים...",
			textareaLabel: "אזור טקסט",
			textareaPlaceholder: "כתוב תגובה...",
			characterCount: "0/280",
			post: "פרסם",
			textareaDescription: "כותרת תחתונה ממוקמת מתחת לאזור הטקסט.",
		},
	},
};

export function InputGroupRtl() {
	const { dir, language, t } = useTranslation(translations, "ar");

	return (
		<div className="grid w-full max-w-sm gap-6" lang={language} dir={dir}>
			<InputGroup className="max-w-xs">
				<InputGroupInput placeholder={t.placeholder} />
				<InputGroupAddon>
					<Search />
				</InputGroupAddon>
				<InputGroupAddon align="inline-end">{t.results}</InputGroupAddon>
			</InputGroup>
			<InputGroup>
				<InputGroupInput placeholder={t.searching} />
				<InputGroupAddon align="inline-end">
					<Spinner />
				</InputGroupAddon>
			</InputGroup>
			<InputGroup>
				<InputGroupInput placeholder={t.savingChanges} />
				<InputGroupAddon align="inline-end">
					<InputGroupText>{t.saving}</InputGroupText>
					<Spinner />
				</InputGroupAddon>
			</InputGroup>
			<FieldGroup className="max-w-sm">
				<Field>
					<FieldLabel htmlFor="rtl-textarea">{t.textareaLabel}</FieldLabel>
					<InputGroup>
						<InputGroupTextarea
							id="rtl-textarea"
							placeholder={t.textareaPlaceholder}
						/>
						<InputGroupAddon align="block-end">
							<InputGroupText>{t.characterCount}</InputGroupText>
							<InputGroupButton variant="default" size="sm" className="ms-auto">
								{t.post}
							</InputGroupButton>
						</InputGroupAddon>
					</InputGroup>
					<FieldDescription>{t.textareaDescription}</FieldDescription>
				</Field>
			</FieldGroup>
		</div>
	);
}

API Reference

InputGroup

The main component that wraps inputs and addons.

PropTypeDefault
variant"default" | "secondary"
"default"
classNamestring

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.

PropTypeDefault
classNamestring

InputGroupButton

Displays buttons within input groups.

PropTypeDefault
size""sm" | "icon-sm"
"sm"
variant"default" | "destructive" | "outline" | "secondary" | "ghost" | "link"
"ghost"
classNamestring

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.

PropTypeDefault

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.

PropTypeDefault

InputGroupText

Renders inline static text content within an addon.

PropTypeDefault
classNamestring