Installation
npx shadcn@latest add @eo-n/input-group
Install dependencies
npm install class-variance-authority
Copy component code
Copy and paste the following code into your project:components/ui/input-group.tsx
"use client";
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-group"
role="group"
className={cn(
"group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none",
"h-9 min-w-0 has-[>textarea]:h-auto",
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
className
)}
{...props}
/>
);
}
const inputGroupAddonVariants = cva(
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
{
variants: {
align: {
"inline-start":
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
"inline-end":
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
"block-start":
"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
"block-end":
"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
},
},
defaultVariants: {
align: "inline-start",
},
}
);
function InputGroupAddon({
className,
align = "inline-start",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
role="group"
data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest("button")) {
return;
}
e.currentTarget.parentElement?.querySelector("input")?.focus();
}}
{...props}
/>
);
}
const inputGroupButtonVariants = cva(
"text-sm shadow-none flex gap-2 items-center",
{
variants: {
size: {
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
"icon-xs":
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
},
},
defaultVariants: {
size: "xs",
},
}
);
function InputGroupButton({
className,
type = "button",
variant = "ghost",
size = "xs",
...props
}: Omit<React.ComponentProps<typeof Button>, "size"> &
VariantProps<typeof inputGroupButtonVariants> &
Pick<React.ComponentProps<"button">, "type">) {
return (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
);
}
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
className={cn(
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
);
}
function InputGroupInput({
className,
...props
}: React.ComponentProps<"input">) {
return (
<Input
data-slot="input-group-control"
className={cn(
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
className
)}
{...props}
/>
);
}
function InputGroupTextarea({
className,
...props
}: React.ComponentProps<"textarea">) {
return (
<Textarea
data-slot="input-group-control"
className={cn(
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
className
)}
{...props}
/>
);
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupInput,
InputGroupTextarea,
};
Update imports
Update the import paths to match your project setup.
Import all parts and piece them together.
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupInput,
} from "@/components/ui/input-group";
<InputGroup>
<InputGroupAddon align="inline-start">
<InputGroupText>$</InputGroupText>
</InputGroupAddon>
<InputGroupInput type="number" placeholder="0.00" />
<InputGroupAddon align="inline-end">
<InputGroupText>USD</InputGroupText>
</InputGroupAddon>
</InputGroup>
Examples
With Icon
<InputGroup>
<InputGroupAddon align="inline-start">
<Search className="h-4 w-4" />
</InputGroupAddon>
<InputGroupInput type="search" placeholder="Search..." />
</InputGroup>
With Button
<InputGroup>
<InputGroupInput type="email" placeholder="Enter your email" />
<InputGroupAddon align="inline-end">
<InputGroupButton>Subscribe</InputGroupButton>
</InputGroupAddon>
</InputGroup>
With Multiple Addons
<InputGroup>
<InputGroupAddon align="inline-start">
<InputGroupText>https://</InputGroupText>
</InputGroupAddon>
<InputGroupInput type="url" placeholder="example.com" />
<InputGroupAddon align="inline-end">
<InputGroupButton size="icon-xs">
<Copy className="h-3.5 w-3.5" />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
Block Start Alignment
<InputGroup>
<InputGroupAddon align="block-start">
<InputGroupText>Description</InputGroupText>
</InputGroupAddon>
<InputGroupTextarea placeholder="Enter description..." rows={4} />
</InputGroup>
Block End Alignment
<InputGroup>
<InputGroupInput placeholder="Enter text..." />
<InputGroupAddon align="block-end">
<InputGroupText className="text-xs">
{characterCount}/200 characters
</InputGroupText>
</InputGroupAddon>
</InputGroup>
With Clear Button
<InputGroup>
<InputGroupInput type="search" placeholder="Search..." value={value} />
{value && (
<InputGroupAddon align="inline-end">
<InputGroupButton size="icon-xs" onClick={() => setValue("")}>
<X className="h-3.5 w-3.5" />
</InputGroupButton>
</InputGroupAddon>
)}
</InputGroup>
Component API
InputGroup
Container that wraps the input and addons.
Additional CSS classes to apply to the input group container.
InputGroupAddon
Container for addon content (text, buttons, icons) around the input.
align
'inline-start' | 'inline-end' | 'block-start' | 'block-end'
default:"inline-start"
Position of the addon relative to the input:
inline-start: Left side (horizontal)
inline-end: Right side (horizontal)
block-start: Top (vertical)
block-end: Bottom (vertical)
Additional CSS classes to apply to the addon.
InputGroupButton
Button component sized for use within input groups.
size
'xs' | 'sm' | 'icon-xs' | 'icon-sm'
default:"xs"
Size variant of the button to fit within the input group.
Visual variant of the button (inherits from Button component).
type
'button' | 'submit' | 'reset'
default:"button"
HTML button type attribute.
InputGroupText
Text content displayed within an addon.
Additional CSS classes to apply to the text element.
InputGroupInput
Input element styled for use within input groups.
Additional CSS classes to apply to the input.
All standard HTML input attributes are supported.
InputGroupTextarea
Textarea element styled for use within input groups.
Additional CSS classes to apply to the textarea.
All standard HTML textarea attributes are supported.
Features
- Flexible Positioning: Place addons at start, end, top, or bottom of inputs
- Multiple Addons: Support for multiple addons on different sides
- Button Integration: Dedicated button component sized for input groups
- Focus Management: Unified focus ring around the entire group
- Error States: Coordinated error styling across the group
- Textarea Support: Works with both single-line inputs and textareas
- Click-to-Focus: Clicking addon areas automatically focuses the input
Accessibility
- Uses
role="group" to indicate related form controls
- Focus states are clearly visible on the entire group
- Error states are properly indicated with ARIA attributes
- Buttons within groups should have appropriate labels
Related Components
- Input - Base input component
- Textarea - Multi-line text input
- Button - Button component for actions