import { SearchIcon } from "lucide-react";
import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group";
import { Kbd } from "@/components/ui/kbd";
export default function InputGroupWithIcon() {
return (
<InputGroup className="max-w-sm">
<InputGroupInput placeholder="Search projects" aria-label="Search projects" />
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<Kbd>⌘K</Kbd>
</InputGroupAddon>
</InputGroup>
);
}- Add the registryOnce per project
pnpm dlx shadcn@latest registry add @jarvis=https://ui.jarv.is/r/{name}.json - Add input-group
pnpm dlx shadcn@latest add @jarvis/input-group
Usage
import { SearchIcon } from "lucide-react";
import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group";
export function SearchInput() {
return (
<InputGroup>
<InputGroupInput placeholder="Search" aria-label="Search" />
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
</InputGroup>
);
}Put the control first and the addons after it. Each addon's align places it around the control.
Label the control itself, with an aria-label or a FieldLabel in a Field.
Composition
Examples
With a spinner
An addon can hold any small element, such as a Spinner.
import { AtSignIcon } from "lucide-react";
import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group";
import { Spinner } from "@/components/ui/spinner";
export default function InputGroupWithSpinner() {
return (
<InputGroup className="max-w-sm">
<InputGroupInput placeholder="ada" aria-label="Username" />
<InputGroupAddon>
<AtSignIcon />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<Spinner />
</InputGroupAddon>
</InputGroup>
);
}With text
InputGroupText adds a prefix or suffix, such as a currency or a unit.
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
} from "@/components/ui/input-group";
export default function InputGroupWithText() {
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<InputGroup>
<InputGroupInput placeholder="acme" aria-label="Subdomain" />
<InputGroupAddon>
<InputGroupText>https://</InputGroupText>
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<InputGroupText>.example.com</InputGroupText>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput inputMode="decimal" placeholder="0.00" aria-label="Monthly budget" />
<InputGroupAddon>
<InputGroupText>$</InputGroupText>
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<InputGroupText>USD</InputGroupText>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput inputMode="numeric" defaultValue="25" aria-label="Seat limit" />
<InputGroupAddon>
<InputGroupText>Seat limit</InputGroupText>
</InputGroupAddon>
</InputGroup>
</div>
);
}With buttons
InputGroupButton is a small button that fits the group. Give icon-only buttons an aria-label.
import { CopyIcon, GlobeIcon } from "lucide-react";
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/components/ui/input-group";
import { toast } from "@/components/ui/toast";
export default function InputGroupWithButton() {
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<InputGroup>
<InputGroupInput
readOnly
defaultValue="https://example.com/share/website-redesign"
aria-label="Share link"
/>
<InputGroupAddon>
<GlobeIcon />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<InputGroupButton
size="icon-xs"
aria-label="Copy link"
onClick={() => toast.add({ title: "Link copied" })}
>
<CopyIcon />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="Filter by name" aria-label="Filter" />
<InputGroupAddon align="inline-end">
<InputGroupButton>Search</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
);
}Invalid
Set aria-invalid on the control to outline the whole group.
import { InfoIcon, MailIcon } from "lucide-react";
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/components/ui/input-group";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
export default function InputGroupInvalid() {
return (
<InputGroup className="max-w-sm">
<InputGroupInput aria-invalid defaultValue="sam@example" aria-label="Email" />
<InputGroupAddon>
<MailIcon />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<Tooltip>
<TooltipTrigger
render={<InputGroupButton size="icon-xs" aria-label="Why is this invalid?" />}
>
<InfoIcon />
</TooltipTrigger>
<TooltipContent>Enter a full email address</TooltipContent>
</Tooltip>
</InputGroupAddon>
</InputGroup>
);
}With a textarea
Use InputGroupTextarea in place of the input. Align addons to block-start or block-end to put
them above or below it.
import { ArrowUpIcon, PaperclipIcon } from "lucide-react";
import * as React from "react";
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupTextarea,
} from "@/components/ui/input-group";
import { toast } from "@/components/ui/toast";
export default function InputGroupWithTextarea() {
const [message, setMessage] = React.useState("");
return (
<InputGroup className="max-w-md">
<InputGroupTextarea
placeholder="Leave a comment…"
aria-label="Comment"
value={message}
onChange={(event) => setMessage(event.target.value)}
maxLength={280}
/>
<InputGroupAddon align="block-end">
<InputGroupButton size="icon-xs" aria-label="Attach a file">
<PaperclipIcon />
</InputGroupButton>
<InputGroupText className="ml-auto tabular-nums">{message.length}/280</InputGroupText>
<InputGroupButton
variant="secondary"
size="icon-xs"
aria-label="Send"
disabled={!message.trim()}
focusableWhenDisabled
onClick={() => {
toast.add({ title: "Comment posted", type: "success" });
setMessage("");
}}
>
<ArrowUpIcon />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
);
}Large
Set size="lg" for a taller group.
import { Button } from "@/components/ui/button";
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
} from "@/components/ui/input-group";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { toast } from "@/components/ui/toast";
const paymentMethods = { visa: "Visa •• 7391", amex: "Amex •• 1008" };
export default function InputGroupLarge() {
return (
<div className="flex w-full max-w-md flex-wrap items-center gap-2">
<InputGroup size="lg" className="w-auto min-w-40 flex-1">
<InputGroupInput inputMode="decimal" defaultValue="50.00" aria-label="Amount in USD" />
<InputGroupAddon>
<InputGroupText>$</InputGroupText>
</InputGroupAddon>
</InputGroup>
<Select items={paymentMethods} defaultValue="visa">
<SelectTrigger size="lg" aria-label="Payment method">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(paymentMethods).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="secondary"
size="lg"
onClick={() => toast.add({ title: "Funds added", type: "success" })}
>
Add funds
</Button>
</div>
);
}Props
InputGroupInput and InputGroupTextarea are an Input and a
Textarea.
| Part | Extends |
|---|---|
| InputGroup | <div> |
| InputGroupAddon | <div> |
| InputGroupButton | Base UI's Button |
| InputGroupText | <span> |
| InputGroupInput | <input> |
| InputGroupTextarea | <textarea> |
The tables leave out className, style and render. Parts built on Base UI take them as its styling and composition guides describe.
InputGroup
| Prop | Type | Default |
|---|---|---|
size | "default" | "lg" | "default" |
InputGroupAddon
| Prop | Type | Default |
|---|---|---|
align | "block-end" | "block-start" | "inline-end" | "inline-start" | "inline-start" |
InputGroupButton
| Prop | Type | Default |
|---|---|---|
nativeButtonWhether the component renders a native | boolean | true |
focusableWhenDisabledWhether the button should be focusable when disabled. | boolean | false |
variant | "destructive" | "ghost" | "link" | "outline" | "primary" | "secondary" | "ghost" |
size | "icon-sm" | "icon-xs" | "sm" | "xs" | "xs" |
type | "button" | "reset" | "submit" | "button" |