Input Group
@anxndsgn/ink-ui
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.

https://
.com
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

PropTypeDefaultDescription

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

PropTypeDefaultDescription

InputGroupAddon

PropTypeDefaultDescription
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:

PropTypeDefaultDescription
size"xs" | "sm" | "icon-xs" | "icon-sm""xs"Compact button size for use inside an input group.

InputGroupText

PropTypeDefaultDescription