.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.Addon><MagnifyingGlassIcon /></InputGroup.Addon>
  <InputGroup.Input placeholder="Search for a domain name" aria-label="Domain search" />
  <InputGroup.Button variant="primary">Search</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:

MatchDescription
valueMissingRequired field is empty
typeMismatchValue doesn’t match type (e.g., invalid email)
patternMismatchValue doesn’t match pattern attribute
tooShortValue shorter than minLength
tooLongValue longer than maxLength
rangeUnderflowValue less than min
rangeOverflowValue greater than max
trueAlways 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.