.workers.dev
<InputGroup>
  <InputGroup.Input aria-label="Subdomain" maxLength={24} />
  <InputGroup.Suffix>.workers.dev</InputGroup.Suffix>
</InputGroup>

Installation

Barrel

import { InputGroup } from "@cloudflare/kumo";

Granular

import { InputGroup } from "@cloudflare/kumo/components/input";

Usage

import { InputGroup } from "@cloudflare/kumo";
import { MagnifyingGlassIcon } from "@phosphor-icons/react";

export default function Example() {
  return (
    <InputGroup>
      <InputGroup.Addon>
        <MagnifyingGlassIcon className="text-kumo-subtle" />
      </InputGroup.Addon>
      <InputGroup.Input placeholder="Search..." aria-label="Search" />
    </InputGroup>
  );
}

Examples

Icon

Use Addon to place icons at the start or end of the input.

<>
  {/* Start icon */}
  <InputGroup>
    <InputGroup.Addon>
      <LinkIcon className="text-kumo-subtle" />
    </InputGroup.Addon>
    <InputGroup.Input placeholder="Paste a link..." aria-label="Link" />
  </InputGroup>

  {/* End icon */}
  <InputGroup>
    <InputGroup.Input placeholder="Add a tag..." aria-label="Tag" />
    <InputGroup.Addon align="end">
      <TagIcon className="text-kumo-subtle" />
    </InputGroup.Addon>
  </InputGroup>

  {/* Both sides */}
  <InputGroup>
    <InputGroup.Addon>
      <AirplaneTakeoffIcon className="text-kumo-subtle" />
    </InputGroup.Addon>
    <InputGroup.Input placeholder="IATA airport code (e.g. GRU, AMS)" aria-label="IATA airport code" />
    <InputGroup.Addon align="end">
      <InfoIcon className="text-kumo-subtle" />
    </InputGroup.Addon>
  </InputGroup>
</>

Text

Use Addon to place text prefixes or suffixes alongside the input.

@
@example.com
/api/
.json
<>
  {/* Start only */}
  <InputGroup>
    <InputGroup.Addon>@</InputGroup.Addon>
    <InputGroup.Input placeholder="username" aria-label="Username" />
  </InputGroup>

  {/* End only */}
  <InputGroup>
    <InputGroup.Input placeholder="email" aria-label="Email" />
    <InputGroup.Addon align="end">@example.com</InputGroup.Addon>
  </InputGroup>

  {/* Both sides */}
  <InputGroup>
    <InputGroup.Addon>/api/</InputGroup.Addon>
    <InputGroup.Input placeholder="endpoint" aria-label="API path" />
    <InputGroup.Addon align="end">.json</InputGroup.Addon>
  </InputGroup>
</>

Button

Place InputGroup.Button inside an Addon for compact inset buttons, or directly as a child for a full-height flush button.

<>
  {/* Icon button inside Addon (compact, inset) */}
  <InputGroup>
    <InputGroup.Input
      type={show ? "text" : "password"}
      defaultValue="password"
      aria-label="Password"
    />
    <InputGroup.Addon align="end">
      <InputGroup.Button
        variant="ghost"
        size="sm"
        aria-label={show ? "Hide password" : "Show password"}
        onClick={() => {}}
      >
        {show ? <EyeSlashIcon size={14} /> : <EyeIcon size={14} />}
      </InputGroup.Button>
    </InputGroup.Addon>
  </InputGroup>

  {/* Text button inside Addon (compact, inset) */}
  <InputGroup>
    <InputGroup.Input placeholder="Filter by name..." aria-label="Filter" />
    <InputGroup.Addon align="end">
      <InputGroup.Button variant="secondary">Apply</InputGroup.Button>
    </InputGroup.Addon>
  </InputGroup>

  {/* Button as direct child (full-height, flush) */}
  <InputGroup>
    <InputGroup.Input placeholder="example.com" aria-label="Domain" />
    <InputGroup.Button variant="primary">Submit</InputGroup.Button>
  </InputGroup>
</>

Kbd

Place a keyboard shortcut hint inside an end Addon.

⌘K
<InputGroup className="w-xs">
  <InputGroup.Addon>
    <MagnifyingGlassIcon className="text-kumo-subtle" />
  </InputGroup.Addon>
  <InputGroup.Input placeholder="Search..." aria-label="Search" />
  <InputGroup.Addon align="end">
    <kbd className="rounded border border-kumo-line bg-kumo-recessed px-1.5 py-0.5 text-xs text-kumo-subtle">
      ⌘K
    </kbd>
  </InputGroup.Addon>
</InputGroup>

Loading

Place a Loader inside an Addon at the start or end. Combine with a text span for a status label.

Saving...
<>
  {/* Spinner at end */}
  <InputGroup>
    <InputGroup.Input placeholder="Searching..." aria-label="Searching" />
    <InputGroup.Addon align="end">
      <Loader />
    </InputGroup.Addon>
  </InputGroup>

  {/* Spinner at start */}
  <InputGroup>
    <InputGroup.Addon>
      <SpinnerIcon className="animate-spin" />
    </InputGroup.Addon>
    <InputGroup.Input placeholder="Thinking..." aria-label="Thinking" />
  </InputGroup>

  {/* Text + spinner at end */}
  <InputGroup>
    <InputGroup.Input placeholder="Saving changes..." aria-label="Saving changes" />
    <InputGroup.Addon align="end">
      <span>Saving...</span>
      <Loader />
    </InputGroup.Addon>
  </InputGroup>
</>

Inline Suffix

Suffix renders text that flows seamlessly next to the typed value — useful for domain inputs like .workers.dev. Truncates with ellipsis when space is limited.

.workers.dev
<InputGroup className="w-xs">
  <InputGroup.Input aria-label="Subdomain" maxLength={24} />
  <InputGroup.Suffix>.workers.dev</InputGroup.Suffix>
  <InputGroup.Addon align="end">
    <CheckCircleIcon weight="duotone" className="text-kumo-brand" />
  </InputGroup.Addon>
</InputGroup>

Sizes

Four sizes: xs, sm, base (default), and lg. The size applies to the entire group. Use the label prop on InputGroup for built-in Field support.

<>
  {/* Extra small */}
  <InputGroup size="xs" label="Extra Small">
    <InputGroup.Addon>
      <MagnifyingGlassIcon className="text-kumo-subtle" />
    </InputGroup.Addon>
    <InputGroup.Input placeholder="Extra small input" />
  </InputGroup>

  {/* Small */}
  <InputGroup size="sm" label="Small">
    <InputGroup.Addon>
      <MagnifyingGlassIcon className="text-kumo-subtle" />
    </InputGroup.Addon>
    <InputGroup.Input placeholder="Small input" />
  </InputGroup>

  {/* Base (default) */}
  <InputGroup label="Base (default)">
    <InputGroup.Addon>
      <MagnifyingGlassIcon className="text-kumo-subtle" />
    </InputGroup.Addon>
    <InputGroup.Input placeholder="Base input" />
  </InputGroup>

  {/* Large */}
  <InputGroup size="lg" label="Large">
    <InputGroup.Addon>
      <MagnifyingGlassIcon className="text-kumo-subtle" />
    </InputGroup.Addon>
    <InputGroup.Input placeholder="Large input" />
  </InputGroup>
</>

States

Various input states including error, disabled, and with description. Pass label, error, and description props directly to InputGroup.

@example.com
Please enter a valid email address

Must be at least 8 characters

<>
  {/* Error state */}
  <InputGroup
    label="Error State"
    error={{ message: "Please enter a valid email address", match: true }}
  >
    <InputGroup.Input
      type="email"
      defaultValue="invalid-email"
    />
    <InputGroup.Addon align="end">@example.com</InputGroup.Addon>
  </InputGroup>

  {/* Disabled */}
  <InputGroup label="Disabled" disabled>
    <InputGroup.Addon>
      <MagnifyingGlassIcon className="text-kumo-subtle" />
    </InputGroup.Addon>
    <InputGroup.Input placeholder="Search..." />
    <InputGroup.Button variant="primary">Search</InputGroup.Button>
  </InputGroup>

  {/* With description and tooltip */}
  <InputGroup
    label="With Description"
    description="Must be at least 8 characters"
    labelTooltip="Your password is stored securely"
  >
    <InputGroup.Input
      type={show ? "text" : "password"}
      placeholder="Enter password"
    />
    <InputGroup.Addon align="end">
      <InputGroup.Button
        variant="ghost"
        size="sm"
        aria-label={show ? "Hide password" : "Show password"}
        onClick={() => {}}
      >
        {show ? <EyeSlashIcon size={14} /> : <EyeIcon size={14} />}
      </InputGroup.Button>
    </InputGroup.Addon>
  </InputGroup>
</>

API Reference

InputGroup

The root container that provides context to all child components. Accepts Field props (label, description, error) and wraps content in a Field when label is provided.

PropTypeDefaultDescription
labelReactNode-The label content — can be a string or any React node.
descriptionReactNode-Helper text displayed below the control (hidden when `error` is present).
errorobject-Validation error with a message and a browser `ValidityState` match key.
requiredboolean-When explicitly `false`, shows gray "(optional)" text after the label. When `true` or `undefined`, no indicator is shown.
labelTooltipReactNode-Tooltip content displayed next to the label via an info icon.
classNamestring--
size"xs" | "sm" | "base" | "lg"--
disabledboolean--
focusMode"container" | "individual"--

InputGroup.Input

The text input element. Inherits size, disabled, and error from InputGroup context. Accepts all standard input attributes except Field-related props which are handled by the parent.

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

InputGroup.Addon

Container for icons, text, or compact buttons positioned at the start or end of the input.

PropTypeDefault
align"start" | "end"-
classNamestring-
childrenReactNode-

InputGroup.Suffix

Inline text that flows seamlessly next to the typed value (e.g., .workers.dev). The input width adjusts automatically as the user types.

PropTypeDefault
classNamestring-
childrenReactNode-

Validation Error Types

When using error as an object, the match property corresponds to HTML5 ValidityState values:

Match Description
valueMissing Required field is empty
typeMismatch Value doesn't match type (e.g., invalid email)
patternMismatch Value doesn't match pattern attribute
tooShort Value shorter than minLength
tooLong Value longer than maxLength
rangeUnderflow Value less than min
rangeOverflow Value greater than max
true Always show error (for server-side validation)

Accessibility

Label Requirement

InputGroup requires an accessible name via one of:

  • label prop on InputGroup (renders a visible label with built-in Field support)
  • aria-label on InputGroup.Input for inputs without a visible label
  • aria-labelledby on InputGroup.Input for custom label association

Missing accessible names trigger console warnings in development.

Group Role

InputGroup automatically renders with role="group", which semantically associates the input with its addons for assistive technologies.