# Input Group

> Add visible context or actions around an input without separating them from the control.

Source: https://www.honestui.com/docs/components/input-group

---
title: Input Group
description: Add visible context or actions around an input without separating them from the control.
---

```tsx
"use client";

import * as React from "react";
import { Search as SearchIcon, X as XIcon } from "honestui/icons";

import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupInput,
} from "@/components/honest-ui/ui/input-group";

export function InputGroupSearch() {
  const [query, setQuery] = React.useState("");

  return (
    <div className="w-full max-w-sm space-y-2">
      <label className="text-sm font-medium" htmlFor="component-search">
        Search components
      </label>
      <InputGroup>
        <InputGroupAddon>
          <SearchIcon aria-hidden="true" />
        </InputGroupAddon>
        <InputGroupInput
          id="component-search"
          value={query}
          onChange={(event) => setQuery(event.target.value)}
          placeholder="Try “dialog”"
          type="search"
        />
        {query && (
          <InputGroupAddon align="inline-end">
            <InputGroupButton
              aria-label="Clear search"
              onClick={() => setQuery("")}
              size="icon-xs"
            >
              <XIcon aria-hidden="true" />
            </InputGroupButton>
          </InputGroupAddon>
        )}
      </InputGroup>
      <p className="text-sm text-muted-foreground" aria-live="polite">
        {query ? `Current query: ${query}` : "Enter a component name."}
      </p>
    </div>
  );
}

```

## Overview

Input Group places an input or textarea inside one shared border with supporting text, icons, or buttons. It is useful when the surrounding content changes how people interpret or operate the value, such as a fixed URL prefix, a search icon, or a clear button.

The group is visual structure, not a replacement for a field. Give the control a visible label and use [Field](/docs/components/field) when you also need a description, validation message, required indicator, or shared disabled state.

## Anatomy

`InputGroup` is the outer container. Add one `InputGroupInput` or `InputGroupTextarea`, then place `InputGroupAddon` before or after it. An addon can contain `InputGroupText` for non-interactive context or `InputGroupButton` for an action.

Inline addons sit at the start or end of a single-line input. Block addons sit above or below the control and let a textarea or input grow to its natural height.

## Behavior

Clicking a non-interactive addon focuses the input. Clicking a button preserves the button action. The border reflects focus, invalid, and disabled states from the control, so put `aria-invalid` and `disabled` on the input or textarea itself.

Prefix and suffix text is not submitted with the input value. If the server needs the complete value, combine the visible context with the submitted value in your application logic.

## Accessibility

Keep a visible label outside the group. Decorative icons need `aria-hidden="true"`; icon-only buttons need an `aria-label` that names the action. Do not put required instructions or error messages only inside an addon, because addons are not automatically associated descriptions.

Use native input types and attributes where possible. Keep the control usable at 200% zoom, and avoid adding so many inline actions that the text entry area becomes too narrow.

## Installation






```bash
npx honestui@latest add input-group
```







Copy the Input Group component into your project.

### components/ui/input-group.tsx

```tsx
"use client"

import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"
import { Button } from "./button"
import { Input } from "./input"
import { Textarea } from "./textarea"

function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="input-group"
      role="group"
      className={cn(
        "group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
        className
      )}
      {...props}
    />
  )
}

const inputGroupAddonVariants = cva(
  "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
  {
    variants: {
      align: {
        "inline-start":
          "order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
        "inline-end":
          "order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
        "block-start":
          "order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
        "block-end":
          "order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
      },
    },
    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(
  "flex items-center gap-2 text-sm shadow-none",
  {
    variants: {
      size: {
        xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
        sm: "",
        "icon-xs":
          "size-6 rounded-[calc(var(--radius)-3px)] 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" | "type"> &
  VariantProps<typeof inputGroupButtonVariants> & {
    type?: "button" | "submit" | "reset"
  }) {
  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(
        "flex items-center gap-2 text-sm text-muted-foreground [&_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 ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled: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-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
        className
      )}
      {...props}
    />
  )
}

export {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupText,
  InputGroupInput,
  InputGroupTextarea,
}

```


  Make sure the local Button, Input, Textarea, and utility imports match your
  project.








## Usage

```tsx
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
  InputGroupText,
} from "@/components/ui/input-group";
```

```tsx
<InputGroup>
  <InputGroupAddon>
    <InputGroupText>https://</InputGroupText>
  </InputGroupAddon>
  <InputGroupInput aria-label="Website address" name="website" />
</InputGroup>
```

## Examples

These examples focus on the relationships that are unique to Input Group: an action beside a control, contextual text that is not part of the value, and a block addon below a textarea.

### Prefix and suffix

```tsx
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
  InputGroupText,
} from "@/components/honest-ui/ui/input-group";

export function InputGroupUrl() {
  return (
    <div className="w-full max-w-sm space-y-2">
      <label className="text-sm font-medium" htmlFor="project-slug">
        Project URL
      </label>
      <InputGroup>
        <InputGroupAddon>
          <InputGroupText>honestui.com/</InputGroupText>
        </InputGroupAddon>
        <InputGroupInput
          id="project-slug"
          name="slug"
          placeholder="my-project"
          spellCheck={false}
        />
        <InputGroupAddon align="inline-end">
          <InputGroupText>.tsx</InputGroupText>
        </InputGroupAddon>
      </InputGroup>
      <p className="text-sm text-muted-foreground">
        Prefixes and suffixes add context; the input value remains only the
        slug.
      </p>
    </div>
  );
}

```

### Textarea with a block addon

```tsx
import {
  InputGroup,
  InputGroupAddon,
  InputGroupText,
  InputGroupTextarea,
} from "@/components/honest-ui/ui/input-group";

export function InputGroupTextareaExample() {
  return (
    <div className="w-full max-w-sm space-y-2">
      <label className="text-sm font-medium" htmlFor="release-note">
        Release note
      </label>
      <InputGroup>
        <InputGroupTextarea
          id="release-note"
          maxLength={180}
          placeholder="Describe what changed"
          rows={4}
        />
        <InputGroupAddon align="block-end" className="justify-end border-t">
          <InputGroupText>180 characters maximum</InputGroupText>
        </InputGroupAddon>
      </InputGroup>
    </div>
  );
}

```

## API reference

- `InputGroup` accepts native `div` props and renders the shared container.
- `InputGroupAddon` accepts native `div` props and `align`: `inline-start`, `inline-end`, `block-start`, or `block-end`.
- `InputGroupButton` accepts Button props and the sizes `xs`, `sm`, `icon-xs`, or `icon-sm`. Its default `type` is `button`.
- `InputGroupText` accepts native `span` props.
- `InputGroupInput` and `InputGroupTextarea` accept their matching native control props.
