Wraps an input with addons (icon, text, or button) into one visual unit — e.g. a read-only 'copy link' field with a button fused to its edge. The source builds this on a separate new/ component family with a different color source than the rest of the site; this bundle normalizes it to compose this system's own Input/Button instead.
npx chandra-asri-ui add input-group<InputGroup>
<InputGroupInput readOnly defaultValue="example.com/r/x7f2" />
<InputGroupAddon align="inline-end">
<InputGroupButton aria-label="Copy link">
<Copy size={14} />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>InputGroupText works the same way for a static prefix like $ or @ instead of a button.
import { Button, type ButtonProps } from "@/app/components/ui/button";
import { Input, type InputProps } from "@/app/components/ui/input";
import { Textarea, type TextareaProps } from "@/app/components/ui/textarea";
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "react";
/**
* The base guideline InputGroup is built on a separate "new/" family of
* Button/Input/Textarea with a different color source (theme tokens like
* `border-input`/`bg-primary`) than the main Input/Button used everywhere
* else — flagged in the source's own style guide as an inconsistency. This
* bundle normalizes InputGroup to compose the *same* Input/Button/Textarea
* as the rest of this system instead of reproducing a second component
* family, so everything shares one design system.
*/
export function InputGroup({
className,
invalid,
...props
}: HTMLAttributes<HTMLDivElement> & { invalid?: boolean }) {
return (
<div
data-invalid={invalid}
className={cn(
"group/input-group border-border relative flex h-9 w-full items-center rounded-md border transition-[color,box-shadow] has-[>textarea]:h-auto",
"has-[[data-slot=input-group-control]:focus-visible]:ring-primary-30 has-[[data-slot=input-group-control]:focus-visible]:ring-1",
invalid && "border-danger-70",
className,
)}
{...props}
/>
);
}
type InputGroupAddonAlign =
"inline-start" | "inline-end" | "block-start" | "block-end";
const alignStyles: Record<InputGroupAddonAlign, string> = {
"inline-start": "order-first pl-3",
"inline-end": "order-last pr-3",
"block-start": "order-first w-full px-3 pt-2",
"block-end": "order-last w-full px-3 pb-2",
};
export function InputGroupAddon({
className,
align = "inline-start",
...props
}: HTMLAttributes<HTMLDivElement> & { align?: InputGroupAddonAlign }) {
return (
<div
className={cn(
"text-muted flex h-auto items-center justify-center gap-2 py-1.5 text-sm font-medium select-none",
alignStyles[align],
className,
)}
{...props}
/>
);
}
export function InputGroupButton({ className, ...props }: ButtonProps) {
return (
<Button
size="xs"
variant="ghost"
className={cn("h-7 px-2", className)}
{...props}
/>
);
}
export function InputGroupText({
className,
...props
}: HTMLAttributes<HTMLSpanElement>) {
return (
<span
className={cn("text-muted flex items-center gap-2 text-sm", className)}
{...props}
/>
);
}
export function InputGroupInput({
className,
inputClassName,
...props
}: InputProps) {
return (
<Input
data-slot="input-group-control"
className={cn("flex-1", className)}
inputClassName={cn(
"rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0",
inputClassName,
)}
{...props}
/>
);
}
export function InputGroupTextarea({ className, ...props }: TextareaProps) {
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",
className,
)}
{...props}
/>
);
}