Skip to content
UI

Input Group

An input or textarea with icons, text or buttons attached inside its border.

@zenoxai/ui/components/input-group Markdown
/
function InputGroupDemo() {
  const [query, setQuery] = React.useState("")

  return (
    <InputGroup className="max-w-xs">
      <InputGroupInput
        type="search"
        value={query}
        onChange={(event) => setQuery(event.target.value)}
        placeholder="Search alerts"
        aria-label="Search alerts"
      />
      <InputGroupAddon>
        <SearchIcon />
      </InputGroupAddon>
      <InputGroupAddon align="inline-end">
        {query ? (
          <InputGroupButton
            size="icon-xs"
            aria-label="Clear search"
            onClick={() => setQuery("")}
          >
            <XIcon />
          </InputGroupButton>
        ) : (
          <Kbd>/</Kbd>
        )}
      </InputGroupAddon>
    </InputGroup>
  )
}

Usage

import { SearchIcon } from "lucide-react"

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

<InputGroup>
  <InputGroupInput placeholder="Search alerts" aria-label="Search alerts" />
  <InputGroupAddon>
    <SearchIcon />
  </InputGroupAddon>
</InputGroup>

InputGroup draws one border around a control and its addons, and moves the focus ring and the invalid ring from the control to the group. Use InputGroupInput or InputGroupTextarea inside it, not the plain Input or Textarea, which would draw a second border.

Put the control first and the addons after it. align on InputGroupAddon places the addon, whatever its order in the markup:

align Places the addon
inline-start (default) Before the control, on the same line.
inline-end After the control, on the same line.
block-start Above the control, full width. The group grows in height.
block-end Below the control, full width. The group grows in height.

A click on an addon outside its buttons focuses the input. Keep addons short: an icon, a unit, a prefix, a keyboard hint or one or two buttons. For a label and help text, use a Field around the group.

The example at the top of the page is the search field of a list: a search icon at the start, and at the end the / shortcut hint, replaced by a “Clear search” button once there is text. Browsers’ own clear buttons on type="search" are hidden by the base styles, so this button is the only one.

Examples

Sizes

size sets the height of a one-line group: sm (28px), default (32px) and lg (36px), the heights of Input and Button. Icons in an addon are 16px, 14px in an sm group.

<div className="flex w-full max-w-xs flex-col gap-3">
  <InputGroup size="sm">
    <InputGroupInput
      placeholder="Filter assets"
      aria-label="Filter assets"
    />
    <InputGroupAddon>
      <SearchIcon />
    </InputGroupAddon>
  </InputGroup>
  <InputGroup>
    <InputGroupInput
      placeholder="Search alerts"
      aria-label="Search alerts"
    />
    <InputGroupAddon>
      <SearchIcon />
    </InputGroupAddon>
  </InputGroup>
  <InputGroup size="lg">
    <InputGroupInput
      placeholder="Search domains"
      aria-label="Search domains"
    />
    <InputGroupAddon>
      <SearchIcon />
    </InputGroupAddon>
  </InputGroup>
</div>

Prefix and suffix text

InputGroupText shows fixed text, such as a protocol, a domain or a unit, that is part of the value’s format but not typed by the user.

https://
.zenox.io
<InputGroup className="max-w-sm">
  <InputGroupInput placeholder="acme" aria-label="Workspace subdomain" />
  <InputGroupAddon>
    <InputGroupText>https://</InputGroupText>
  </InputGroupAddon>
  <InputGroupAddon align="inline-end">
    <InputGroupText>.zenox.io</InputGroupText>
  </InputGroupAddon>
</InputGroup>

With a button

InputGroupButton is a Button sized for the group: xs (24px, the default) and sm (28px) for buttons with text, icon-xs and icon-sm for icon-only buttons. Each size has the text, icon and radius of the Button size of the same name. The 28px sizes fit the default and lg groups; in an sm group use the xs sizes. It defaults to variant="ghost" and type="button", so it does not submit the form.

<InputGroup className="max-w-sm">
  <InputGroupInput
    readOnly
    defaultValue="https://hooks.zenox.io/t/acme/alerts"
    aria-label="Alert webhook URL"
  />
  <InputGroupAddon align="inline-end">
    <InputGroupButton size="icon-xs" aria-label="Copy webhook URL">
      <CopyIcon />
    </InputGroupButton>
  </InputGroupAddon>
</InputGroup>

With a textarea

A block-end addon under an InputGroupTextarea holds a hint and the submit button of a comment or note box.

Visible to everyone in the tenant
<InputGroup className="max-w-md">
  <InputGroupTextarea
    placeholder="Add a note for the next analyst"
    aria-label="Investigation note"
    rows={3}
  />
  <InputGroupAddon align="block-end">
    <InputGroupText>Visible to everyone in the tenant</InputGroupText>
    <InputGroupButton variant="default" size="xs" className="ml-auto">
      <SendIcon data-icon="inline-start" />
      Add note
    </InputGroupButton>
  </InputGroupAddon>
</InputGroup>

Accessibility

  • The control needs its own name: aria-label, or a FieldLabel pointing at its id. Text in an addon, such as https://, is not part of that name; repeat it in the label when it matters.
  • Icon-only InputGroupButtons need an aria-label. Lucide icons render with aria-hidden="true", so an icon addon adds nothing to what screen readers announce.
  • The addon’s click-to-focus is for pointer users only; keyboard users tab to the input directly.
  • To mark the group invalid, set aria-invalid on the control. The group draws the error ring.

API reference

Generated from the TypeScript source. className, style and children are accepted everywhere and omitted below.

InputGroup

InputGroup attaches icons, text, key hints or buttons to an input or a textarea inside one bordered box: a search field with its icon, a URL with its prefix, a message box with a send button. Use InputGroupInput or InputGroupTextarea as the control and an InputGroupAddon per attachment.

PropTypeDefault
size"default" | "sm" | "lg"

Height of the group on one row: `sm` 28px, `default` 32px, `lg` 36px, the heights of Input and Button. An `sm` group fits `xs` buttons only.

default

InputGroupAddon

InputGroupAddon holds the icons, text or buttons attached to the control. A click on its empty area focuses the input.

PropTypeDefault
align"inline-end" | "inline-start" | "block-start" | "block-end" | null

Where the addon sits: `inline-start` (default) and `inline-end` before and after the control on its row; `block-start` and `block-end` as full-width rows above and below it, as around a textarea.

inline-start

InputGroupButton

InputGroupButton is a compact button sized to sit in an addon. It defaults to type="button" and the ghost variant, so it never submits the form by accident.

PropTypeDefault
focusableWhenDisabledboolean

Whether the button should be focusable when disabled.

false
nativeButtonboolean

Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`).

true
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ButtonState>

Allows you to replace the component's HTML element with a different tag, or compose it with another component.

-
size"xs" | "sm" | "icon-xs" | "icon-sm"xs
type"button" | "submit" | "reset"button
variant"link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null

Visual weight. `default` is the filled primary action, one per view; `outline` and `secondary` are for the other actions; `ghost` has no fill until hover, for toolbars and icon buttons; `destructive` is for delete and other irreversible actions; `link` looks like inline text.

ghost

InputGroupText

InputGroupText is static text or an icon inside an addon, such as a currency sign or a unit.

No props of its own. Accepts the props of the element it renders.

InputGroupInput

InputGroupInput is the Input to use inside an InputGroup: it drops its own border and focus ring, which the group draws instead.

No props of its own. Accepts the props of the element it renders.

InputGroupTextarea

InputGroupTextarea is the Textarea to use inside an InputGroup: it drops its own border and focus ring, which the group draws instead.

No props of its own. Accepts the props of the element it renders.