Skip to content

InputGroup

Add addons, buttons, and helper content to inputs.

Code
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
SearchIcon,
} from "@falcon/ui-kit";
export function Example() {
return (
<InputGroup className="tw:max-w-xs">
<InputGroupInput aria-label="Search products" placeholder="Search..." />
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
<InputGroupAddon align="inline-end">12 results</InputGroupAddon>
</InputGroup>
);
}

Use the following composition to build an InputGroup:

InputGroup
├── InputGroupInput or InputGroupTextarea
├── InputGroupAddon
├── InputGroupButton
└── InputGroupText

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.

Use align="inline-start" to position the addon at the start of the input. This is the default.

Code
import {
Field,
FieldDescription,
FieldLabel,
InputGroup,
InputGroupAddon,
InputGroupInput,
SearchIcon,
} from "@falcon/ui-kit";
export function Example() {
return (
<Field className="tw:max-w-sm">
<FieldLabel htmlFor="inline-start-input">Product</FieldLabel>
<InputGroup>
<InputGroupInput id="inline-start-input" placeholder="Search..." />
<InputGroupAddon align="inline-start">
<SearchIcon className="tw:text-muted-foreground" />
</InputGroupAddon>
</InputGroup>
<FieldDescription>Icon positioned at the start.</FieldDescription>
</Field>
);
}

Use align="inline-end" to position the addon at the end of the input.

Code
import {
EyeOffIcon,
Field,
FieldDescription,
FieldLabel,
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@falcon/ui-kit";
export function Example() {
return (
<Field className="tw:max-w-sm">
<FieldLabel htmlFor="inline-end-input">Account code</FieldLabel>
<InputGroup>
<InputGroupInput
id="inline-end-input"
type="password"
placeholder="Enter account code"
/>
<InputGroupAddon align="inline-end">
<EyeOffIcon />
</InputGroupAddon>
</InputGroup>
<FieldDescription>Icon positioned at the end.</FieldDescription>
</Field>
);
}

Use align="block-start" to position the addon above the input.

Code
import {
CopyIcon,
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FileTextIcon,
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from "@falcon/ui-kit";
export function Example() {
return (
<FieldGroup className="tw:max-w-sm">
<Field>
<FieldLabel htmlFor="block-start-input">Input</FieldLabel>
<InputGroup className="tw:h-auto">
<InputGroupInput
id="block-start-input"
placeholder="Enter customer name"
/>
<InputGroupAddon align="block-start">
<InputGroupText>Customer 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="Leave delivery instructions"
className="tw:text-sm"
/>
<InputGroupAddon align="block-start">
<FileTextIcon className="tw:text-muted-foreground" />
<InputGroupText>Delivery instructions</InputGroupText>
<InputGroupButton
aria-label="Copy delivery instructions"
size="icon-xs"
className="tw:ml-auto"
>
<CopyIcon />
<span className="tw:sr-only">Copy</span>
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<FieldDescription>
Header positioned above the textarea.
</FieldDescription>
</Field>
</FieldGroup>
);
}

Use align="block-end" to position the addon below the input.

Code
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from "@falcon/ui-kit";
export function Example() {
return (
<FieldGroup className="tw:max-w-sm">
<Field>
<FieldLabel htmlFor="block-end-input">Input</FieldLabel>
<InputGroup className="tw: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 an order note..."
/>
<InputGroupAddon align="block-end">
<InputGroupText>0/280</InputGroupText>
<InputGroupButton
variant="default"
size="sm"
className="tw:ml-auto"
>
Post
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<FieldDescription>
Footer positioned below the textarea.
</FieldDescription>
</Field>
</FieldGroup>
);
}
Code
import {
CheckIcon,
CreditCardIcon,
InfoIcon,
InputGroup,
InputGroupAddon,
InputGroupInput,
MailIcon,
SearchIcon,
StarIcon,
} from "@falcon/ui-kit";
export function Example() {
return (
<div className="tw:grid tw:w-full tw:max-w-sm tw:gap-6">
<InputGroup>
<InputGroupInput aria-label="Search products" placeholder="Search..." />
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput
aria-label="Contact email"
type="email"
placeholder="Enter contact email"
/>
<InputGroupAddon>
<MailIcon />
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput aria-label="Card number" placeholder="Card number" />
<InputGroupAddon>
<CreditCardIcon />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<CheckIcon />
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput
aria-label="Account number"
placeholder="Account number"
/>
<InputGroupAddon align="inline-end">
<StarIcon />
<InfoIcon />
</InputGroupAddon>
</InputGroup>
</div>
);
}
Code
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from "@falcon/ui-kit";
export function Example() {
return (
<div className="tw:grid tw:w-full tw:max-w-sm tw:gap-6">
<InputGroup>
<InputGroupInput aria-label="Amount" placeholder="0.00" />
<InputGroupAddon>
<InputGroupText>$</InputGroupText>
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<InputGroupText>USD</InputGroupText>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput
aria-label="Customer portal"
placeholder="customer-portal"
className="tw:pl-0.5!"
/>
<InputGroupAddon>
<InputGroupText>https://</InputGroupText>
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<InputGroupText>.com</InputGroupText>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput aria-label="Username" placeholder="Enter username" />
<InputGroupAddon align="inline-end">
<InputGroupText>@retailer.com</InputGroupText>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupTextarea aria-label="Message" placeholder="Enter message" />
<InputGroupAddon align="block-end">
<InputGroupText className="tw:text-xs tw:text-muted-foreground">
120 characters left
</InputGroupText>
</InputGroupAddon>
</InputGroup>
</div>
);
}
Code
import { useState } from "react";
import {
CheckIcon,
CopyIcon,
InfoIcon,
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
Popover,
PopoverContent,
PopoverDescription,
PopoverTitle,
PopoverTrigger,
StarIcon,
} from "@falcon/ui-kit";
export function Example() {
const [isCopied, setIsCopied] = useState(false);
const [isFavorite, setIsFavorite] = useState(false);
return (
<div className="tw:grid tw:w-full tw:max-w-sm tw:gap-6">
<InputGroup>
<InputGroupInput
aria-label="Customer portal URL"
placeholder="https://portal.example.com"
readOnly
/>
<InputGroupAddon align="inline-end">
<InputGroupButton
aria-label="Copy"
title="Copy"
size="icon-xs"
onClick={() => {
void navigator.clipboard.writeText("https://portal.example.com");
setIsCopied(true);
}}
>
{isCopied ? <CheckIcon /> : <CopyIcon />}
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<InputGroup className="tw:[--radius:9999px]">
<InputGroupInput
id="customer-portal-address"
aria-label="Portal address"
/>
<InputGroupAddon>
<Popover>
<PopoverTrigger
render={
<InputGroupButton
aria-label="Connection information"
variant="secondary"
size="icon-xs"
/>
}
aria-label="Connection information"
>
<InfoIcon />
</PopoverTrigger>
<PopoverContent
align="start"
className="tw:flex tw:flex-col tw:gap-1 tw:rounded-xl tw:text-sm"
>
<PopoverTitle>Connection information</PopoverTitle>
<PopoverDescription>
Confirm the customer portal address before sharing account data.
</PopoverDescription>
</PopoverContent>
</Popover>
</InputGroupAddon>
<InputGroupAddon className="tw:pl-1.5 tw:text-muted-foreground">
https://
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<InputGroupButton
aria-label={isFavorite ? "Remove favorite" : "Add favorite"}
aria-pressed={isFavorite}
onClick={() => setIsFavorite(!isFavorite)}
size="icon-xs"
>
<StarIcon
data-favorite={isFavorite}
className="tw:data-[favorite=true]:fill-primary tw:data-[favorite=true]:stroke-primary"
/>
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput
aria-label="Search products"
placeholder="Type to search..."
/>
<InputGroupAddon align="inline-end">
<InputGroupButton variant="secondary">Search</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
);
}
Code
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
SearchIcon,
} from "@falcon/ui-kit";
export function Example() {
return (
<InputGroup className="tw:max-w-sm">
<InputGroupInput aria-label="Search products" placeholder="Search..." />
<InputGroupAddon>
<SearchIcon className="tw:text-muted-foreground" />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<kbd className="tw:rounded-sm tw:border tw:border-border tw:bg-muted tw:px-1.5 tw:py-0.5 tw:text-xs tw:text-muted-foreground">
Ctrl K
</kbd>
</InputGroupAddon>
</InputGroup>
);
}
Code
import {
ChevronDownIcon,
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuTrigger,
EllipsisIcon,
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@falcon/ui-kit";
export function Example() {
return (
<div className="tw:grid tw:w-full tw:max-w-sm tw:gap-4">
<InputGroup>
<InputGroupInput
aria-label="Document name"
placeholder="Enter document name"
/>
<InputGroupAddon align="inline-end">
<DropdownMenu>
<DropdownMenuTrigger
render={
<InputGroupButton
variant="ghost"
aria-label="More document actions"
size="icon-xs"
/>
}
>
<EllipsisIcon />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={8} alignOffset={-4}>
<DropdownMenuGroup>
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuItem>Copy reference</DropdownMenuItem>
<DropdownMenuItem>Open location</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput
aria-label="Search query"
placeholder="Enter search query"
/>
<InputGroupAddon align="inline-end">
<DropdownMenu>
<DropdownMenuTrigger
render={
<InputGroupButton
variant="ghost"
className="tw:pr-1.5! tw:text-xs"
/>
}
>
Search in... <ChevronDownIcon className="tw:size-3" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={8} alignOffset={-4}>
<DropdownMenuGroup>
<DropdownMenuItem>Products</DropdownMenuItem>
<DropdownMenuItem>Customers</DropdownMenuItem>
<DropdownMenuItem>Orders</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</InputGroupAddon>
</InputGroup>
</div>
);
}
Code
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
LoaderIcon,
Spinner,
} from "@falcon/ui-kit";
export function Example() {
return (
<div className="tw:grid tw:w-full tw:max-w-sm tw:gap-4">
<InputGroup>
<InputGroupInput
aria-label="Product search"
placeholder="Searching..."
/>
<InputGroupAddon align="inline-end">
<Spinner aria-label="Searching products" />
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput
aria-label="Order import"
placeholder="Processing..."
/>
<InputGroupAddon>
<Spinner aria-label="Processing order import" />
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput
aria-label="Order changes"
placeholder="Saving changes..."
/>
<InputGroupAddon align="inline-end">
<InputGroupText>Saving...</InputGroupText>
<Spinner aria-hidden="true" />
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput
aria-label="Inventory data"
placeholder="Refreshing data..."
/>
<InputGroupAddon>
<LoaderIcon className="tw:animate-spin" aria-hidden="true" />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<InputGroupText className="tw:text-muted-foreground">
Please wait...
</InputGroupText>
</InputGroupAddon>
</InputGroup>
</div>
);
}
Code
import {
ClipboardIcon,
CornerDownLeftIcon,
FileTextIcon,
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupTextarea,
RefreshCwIcon,
} from "@falcon/ui-kit";
export function Example() {
return (
<div className="tw:grid tw:w-full tw:max-w-md tw:gap-4">
<InputGroup>
<InputGroupTextarea
id="delivery-instructions"
aria-label="Delivery instructions"
placeholder="Enter delivery instructions"
className="tw:min-h-[200px]"
/>
<InputGroupAddon align="block-end" className="tw:border-t">
<InputGroupText>Line 1, column 1</InputGroupText>
<InputGroupButton size="sm" className="tw:ml-auto" variant="default">
Save <CornerDownLeftIcon />
</InputGroupButton>
</InputGroupAddon>
<InputGroupAddon align="block-start" className="tw:border-b">
<InputGroupText className="tw:font-medium">
<FileTextIcon />
Delivery instructions
</InputGroupText>
<InputGroupButton
aria-label="Reset delivery instructions"
className="tw:ml-auto"
size="icon-xs"
>
<RefreshCwIcon />
</InputGroupButton>
<InputGroupButton
aria-label="Copy delivery instructions"
variant="ghost"
size="icon-xs"
>
<ClipboardIcon />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
);
}

Add the data-slot="input-group-control" attribute to your custom input for automatic focus state handling.

Here’s an example of a custom auto-resizing textarea using native CSS.

Code
import { InputGroup, InputGroupAddon, InputGroupButton } from "@falcon/ui-kit";
export function Example() {
return (
<div className="tw:grid tw:w-full tw:max-w-sm tw:gap-6">
<InputGroup>
<textarea
data-slot="input-group-control"
aria-label="Order note"
className="tw:flex tw:field-sizing-content tw:min-h-16 tw:w-full tw:resize-none tw:rounded-md tw:bg-transparent tw:px-3 tw:py-2.5 tw:text-base tw:transition-[color,box-shadow] tw:outline-none tw:md:text-sm"
placeholder="Enter an order note..."
/>
<InputGroupAddon align="block-end">
<InputGroupButton className="tw:ml-auto" size="sm" variant="default">
Submit
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
);
}

Groups a form control with contextual addons. Renders a <div> element with role="group" and accepts standard <div> props.

Contains contextual content beside a form control. Renders a <div> element with role="group", accepts standard <div> props, and supports align.

align?: "inline-end" | "inline-start" | "block-start" | "block-end" | null = "inline-start"

Controls the addon’s position relative to the form control. Defaults to "inline-start".

A Button for use in an InputGroup addon. Renders a <button> element by default and accepts standard <button> props. Use render to replace the element. type defaults to "button", variant to "ghost", and size to "xs".

className?: string

CSS class applied to the button.

focusableWhenDisabled?: boolean

Whether a disabled button stays focusable. Defaults to false.

nativeButton?: boolean

Whether the element passed to render is a native button. Defaults to true.

render?: React.ReactElement<any, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ButtonState>

Allows replacing the button element or composing it with another component.

size?: "xs" | "sm" | "icon-xs" | "icon-sm" | null = "xs"

Controls the button’s size. The icon sizes are square and require an accessible name. Defaults to "xs".

style?: React.CSSProperties | ((state: ButtonState) => React.CSSProperties | undefined)

Style applied to the button, or a function based on its state.

type?: "button" | "submit" | "reset" = "button"

The button type. Defaults to "button".

variant?: "outline" | "link" | "secondary" | "default" | "ghost" | "destructive" | null = "ghost"

Controls the button’s appearance only. Defaults to "ghost".

An Input for use in an InputGroup. Renders an <input> element, accepts standard <input> props, and supports render to replace the element.

className?: string

CSS class applied to the input.

defaultValue?: string | number | readonly string[]

The initial value of an uncontrolled input.

onValueChange?: ((value: string, eventDetails: Input.ChangeEventDetails) => void)

Callback fired when the value changes. Use with value for a controlled input.

render?: React.ReactElement<any, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, InputState>

Allows replacing the input element or composing it with another component.

style?: React.CSSProperties | ((state: InputState) => React.CSSProperties | undefined)

Style applied to the input, or a function based on its state.

value?: string | number | readonly string[]

The value of a controlled input.

Provides inline text in an InputGroup addon. Renders a <span> element and accepts standard <span> props.

A Textarea for use in an InputGroup. Renders a <textarea> element and accepts standard <textarea> props.