Input Group
@anxndsgn/ink-uiInput Group
A composable input container for icons, text, buttons, and multiline controls.
import { MagnifyingGlassIcon } from "@phosphor-icons/react";
import { InputGroup, InputGroupAddon, InputGroupInput } from "@registry/components/ui/input-group";
export function InputGroupExample() {
return (
<div className="flex w-full max-w-sm flex-wrap items-center gap-4">
<InputGroup>
<InputGroupInput placeholder="Search components..." />
<InputGroupAddon>
<MagnifyingGlassIcon />
</InputGroupAddon>
</InputGroup>
</div>
);
} Examples
Button
Use InputGroupButton inside an addon for compact actions that belong to the field.
import { PaperPlaneTiltIcon } from "@phosphor-icons/react";
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@registry/components/ui/input-group";
export function InputGroupWithButtonExample() {
return (
<div className="flex w-full max-w-sm flex-wrap items-center gap-4">
<InputGroup>
<InputGroupInput placeholder="Ask Ink UI..." />
<InputGroupAddon align="inline-end">
<InputGroupButton size="icon-xs" aria-label="Send">
<PaperPlaneTiltIcon />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
);
} Text
Use InputGroupText for static prefixes, suffixes, keyboard hints, and short labels.
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "@registry/components/ui/input-group";
export function InputGroupWithTextExample() {
return (
<div className="flex w-full max-w-sm flex-wrap items-center gap-4">
<InputGroup>
<InputGroupAddon>
<InputGroupText>https://</InputGroupText>
</InputGroupAddon>
<InputGroupInput placeholder="ink-ui" />
<InputGroupAddon align="inline-end">
<InputGroupText>.com</InputGroupText>
</InputGroupAddon>
</InputGroup>
</div>
);
} Textarea
Use InputGroupTextarea when the control needs multiline input.
Press Cmd + Enter to send
import { InputGroup, InputGroupAddon, InputGroupText, InputGroupTextarea } from "@registry/components/ui/input-group";
export function InputGroupTextareaExample() {
return (
<div className="flex w-full max-w-sm flex-wrap items-center gap-4">
<InputGroup>
<InputGroupTextarea placeholder="Write a short message..." />
<InputGroupAddon align="block-end">
<InputGroupText>Press Cmd + Enter to send</InputGroupText>
</InputGroupAddon>
</InputGroup>
</div>
);
} Agent chat
A common agent chat composer with attachment, mode controls, voice input, and submit actions.
import { ArrowUpIcon, GearSixIcon, HandPalmIcon, LightbulbIcon, MicrophoneIcon, PlusIcon } from "@phosphor-icons/react";
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupTextarea } from "@registry/components/ui/input-group";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@registry/components/ui/select";
import { Toggle } from "@registry/components/ui/toggle";
export function AgentChatInputGroupExample() {
const permissionItems = [
{ value: "default", label: "Default permissions" },
{ value: "read-only", label: "Read only" },
{ value: "full-access", label: "Full access" },
];
const modelItems = [
{ value: "medium", label: "5.5 Medium" },
{ value: "fast", label: "5.5 Fast" },
{ value: "deep", label: "5.5 Deep" },
];
return (
<div className="flex w-full max-w-2xl flex-wrap items-center gap-4">
<InputGroup className="min-h-36 items-stretch rounded-2xl bg-background">
<InputGroupTextarea
className="min-h-20 py-4 text-base"
placeholder="Ask Ink UI anything. @ to mention files or plugins"
/>
<InputGroupAddon align="block-end" className="flex-wrap justify-between gap-3 pt-3">
<div className="flex flex-wrap items-center gap-2">
<InputGroupButton size="icon-sm" variant="outline" aria-label="Attach file">
<PlusIcon />
</InputGroupButton>
<Select items={permissionItems} defaultValue="default">
<SelectTrigger size="sm" variant="ghost" className="justify-start">
<HandPalmIcon />
<SelectValue />
</SelectTrigger>
<SelectContent>
{permissionItems.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Toggle aria-label="Toggle thinking" defaultPressed>
<LightbulbIcon />
</Toggle>
<InputGroupButton size="icon-sm" variant="ghost" aria-label="Open settings">
<GearSixIcon />
</InputGroupButton>
</div>
<div className="flex flex-wrap items-center gap-2">
<Select items={modelItems} defaultValue="medium">
<SelectTrigger size="sm" variant="ghost" className="justify-between">
<SelectValue />
</SelectTrigger>
<SelectContent>
{modelItems.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
<InputGroupButton size="icon-sm" variant="ghost" aria-label="Use voice input">
<MicrophoneIcon />
</InputGroupButton>
<InputGroupButton size="icon-sm" aria-label="Submit message" variant="default">
<ArrowUpIcon />
</InputGroupButton>
</div>
</InputGroupAddon>
</InputGroup>
</div>
);
} Installation
Copy the source code below into your project:
import { Button } from "@registry/components/ui/button";
import { Input } from "@registry/components/ui/input";
import { Textarea } from "@registry/components/ui/textarea";
import { cn } from "@registry/lib/utils";
import { cva } from "class-variance-authority";
import type { ComponentProps } from "react";
import type { VariantProps } from "class-variance-authority";
function InputGroup({ className, ...props }: ComponentProps<"div">) {
return (
<div
className={cn(
"group/input-group relative flex min-h-9 w-full min-w-0 items-center rounded-lg border border-input bg-gray-950/5 text-foreground transition-all duration-150 outline-none dark:bg-gray-950/30",
"has-[>textarea]:h-auto",
"has-[>[data-align=inline-start]]:*:data-[slot=input-group-control]:pl-2",
"has-[>[data-align=inline-end]]:*:data-[slot=input-group-control]:pr-2",
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:*:data-[slot=input-group-control]:pb-3",
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:*:data-[slot=input-group-control]:pt-3",
"has-[[data-slot=input-group-control]:focus-visible]:border-accent has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring",
"has-[[data-slot=input-group-control][aria-invalid=true]]:border-destructive has-[[data-slot=input-group-control][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot=input-group-control][aria-invalid=true]]:ring-destructive/40",
"has-[[data-slot=input-group-control]:disabled]:cursor-not-allowed has-[[data-slot=input-group-control]:disabled]:opacity-50",
className,
)}
data-slot="input-group"
role="group"
{...props}
/>
);
}
const inputGroupAddonVariants = cva(
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm text-muted-foreground select-none group-has-[[data-slot=input-group-control]:disabled]/input-group:pointer-events-none [&>svg]:pointer-events-none [&>svg:not([class*='size-'])]:size-4",
{
defaultVariants: {
align: "inline-start",
},
variants: {
align: {
"block-end": "order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3",
"block-start": "order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3",
"inline-end": "order-last pr-3 has-[>button]:mr-[-0.35rem]",
"inline-start": "order-first pl-3 has-[>button]:ml-[-0.35rem]",
},
},
},
);
function InputGroupAddon({
align = "inline-start",
className,
onMouseDown,
...props
}: ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
className={cn(inputGroupAddonVariants({ align }), className)}
data-align={align}
data-slot="input-group-addon"
onMouseDown={(event) => {
onMouseDown?.(event);
const target = event.target;
if (!(target instanceof HTMLElement) || !event.currentTarget.contains(target)) {
return;
}
if (event.defaultPrevented || target.closest("button")) {
return;
}
event.preventDefault();
event.currentTarget.parentElement
?.querySelector<HTMLElement>("[data-slot='input-group-control']")
?.focus();
}}
role="group"
{...props}
/>
);
}
const inputGroupButtonVariants = cva("h-7 gap-1.5 rounded-lg px-2 text-sm shadow-none", {
defaultVariants: {
size: "xs",
},
variants: {
size: {
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
"icon-xs": "size-7 p-0 has-[>svg]:p-0",
sm: "h-8 px-2.5 has-[>svg]:px-2.5",
xs: "h-7 px-2 has-[>svg]:px-2",
},
},
});
function InputGroupButton({
className,
size = "xs",
type = "button",
variant = "ghost",
...props
}: Omit<ComponentProps<typeof Button>, "size"> & VariantProps<typeof inputGroupButtonVariants>) {
return (
<Button
className={cn(inputGroupButtonVariants({ size }), className)}
data-size={size}
size={size === "sm" || size === "icon-sm" ? "sm" : "icon-sm"}
type={type}
variant={variant}
{...props}
/>
);
}
function InputGroupText({ className, ...props }: ComponentProps<"span">) {
return (
<span
className={cn(
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className,
)}
data-slot="input-group-text"
{...props}
/>
);
}
type InputGroupInputProps = Omit<ComponentProps<typeof Input>, "block" | "variant">;
function InputGroupInput({ className, ...props }: InputGroupInputProps) {
return (
<Input
block
className={cn(
"min-h-9 min-w-0 flex-1 rounded-none border-0 bg-transparent px-3.5 outline-none focus-visible:border-transparent focus-visible:ring-0 dark:bg-transparent",
className,
)}
data-slot="input-group-control"
{...props}
/>
);
}
function InputGroupTextarea({ className, ...props }: ComponentProps<typeof Textarea>) {
return (
<Textarea
className={cn(
"min-h-24 min-w-0 flex-1 rounded-none border-0 bg-transparent py-3 outline-none focus-visible:border-transparent focus-visible:ring-0 dark:bg-transparent",
className,
)}
data-slot="input-group-control"
{...props}
/>
);
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupInput,
InputGroupTextarea,
}; API Reference
InputGroup
| Prop | Type | Default | Description |
|---|
InputGroupInput
This component does not add any props on top of Base UI Input . See the Base UI docs for the full API reference.
InputGroupTextarea
| Prop | Type | Default | Description |
|---|
InputGroupAddon
| Prop | Type | Default | Description |
|---|---|---|---|
| align | "inline-start" | "inline-end" | "block-start" | "block-end" | "inline-start" | Visual placement of the addon inside the input group. |
InputGroupButton
The InputGroupButton component extends the Ink UI Button
props and adds the following:
| Prop | Type | Default | Description |
|---|---|---|---|
| size | "xs" | "sm" | "icon-xs" | "icon-sm" | "xs" | Compact button size for use inside an input group. |
InputGroupText
| Prop | Type | Default | Description |
|---|