<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.
<>
{/* 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.
<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.
<>
{/* 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.
<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.
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.
| Prop | Type | Default | Description |
|---|---|---|---|
| label | ReactNode | - | The label content — can be a string or any React node. |
| description | ReactNode | - | Helper text displayed below the control (hidden when `error` is present). |
| error | object | - | Validation error with a message and a browser `ValidityState` match key. |
| required | boolean | - | When explicitly `false`, shows gray "(optional)" text after the label. When `true` or `undefined`, no indicator is shown. |
| labelTooltip | ReactNode | - | Tooltip content displayed next to the label via an info icon. |
| className | string | - | - |
| size | "xs" | "sm" | "base" | "lg" | - | - |
| disabled | boolean | - | - |
| 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.
| Prop | Type | Default |
|---|
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.
| Prop | Type | Default |
|---|---|---|
| align | "start" | "end" | - |
| className | string | - |
| children | ReactNode | - |
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.
| Prop | Type | Default |
|---|---|---|
| className | string | - |
| children | ReactNode | - |
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:
-
labelprop on InputGroup (renders a visible label with built-in Field support) -
aria-labelon InputGroup.Input for inputs without a visible label -
aria-labelledbyon 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.