<script lang="ts">
import { CheckCircle } from 'phosphor-svelte';
import { InputGroup, Loader } from 'kumo-svelte';
let status = $state<'idle' | 'loading' | 'success'>('success');
let value = $state('kumo');
let timer: ReturnType<typeof setTimeout> | undefined;
function handleChange(next: string) {
value = next;
if (timer) clearTimeout(timer);
if (next.length > 0) {
status = 'loading';
timer = setTimeout(() => {
status = 'success';
}, 1500);
} else {
status = 'idle';
}
}
</script>
<div class="w-full max-w-2xs">
<InputGroup>
<InputGroup.Input {value} maxlength={20} onValueChange={handleChange} />
<InputGroup.Suffix>.workers.dev</InputGroup.Suffix>
{#if status !== 'idle'}
<InputGroup.Addon align="end">
{#if status === 'loading'}
<Loader />
{:else}
<CheckCircle weight="duotone" class="text-kumo-success" />
{/if}
</InputGroup.Addon>
{/if}
</InputGroup>
</div> Installation
Barrel
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupSuffix } from 'kumo-svelte'; Granular
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupSuffix } from 'kumo-svelte/components/input-group';Usage
With Built-in Field (Recommended)
Pass the label prop to InputGroup to enable the built-in Field wrapper with
label, description, and error support.
<script lang="ts">import { MagnifyingGlass } from "phosphor-svelte"; import { InputGroup } from "kumo-svelte";</script><InputGroup label="Search" description="Find pages, components, and more"> <InputGroup.Addon> <MagnifyingGlass /> </InputGroup.Addon> <InputGroup.Input placeholder="Search..." /></InputGroup> Bare InputGroup (Custom Layouts)
For custom form layouts, use InputGroup without label. Must provide aria-label on InputGroup.Input for accessibility.
<script lang="ts">import { MagnifyingGlass } from "phosphor-svelte"; import { InputGroup } from "kumo-svelte";</script><InputGroup> <InputGroup.Addon> <MagnifyingGlass /> </InputGroup.Addon> <InputGroup.Input placeholder="Search..." aria-label="Search" /></InputGroup>Examples
Icon
Use Addon to place an icon at the start of the input as a visual identifier.
<script lang="ts">
import { LinkIcon } from 'phosphor-svelte';
import { InputGroup } from 'kumo-svelte';
</script>
<InputGroup class="w-full max-w-3xs">
<InputGroup.Addon>
<LinkIcon />
</InputGroup.Addon>
<InputGroup.Input placeholder="Paste a link..." aria-label="Link" />
</InputGroup> Text
Use Addon to place text prefixes or suffixes alongside the input.
<script lang="ts">
import { InputGroup } from 'kumo-svelte';
</script>
<div class="flex flex-col gap-4">
<InputGroup class="w-full max-w-3xs">
<InputGroup.Addon>@</InputGroup.Addon>
<InputGroup.Input placeholder="username" aria-label="Username" />
</InputGroup>
<InputGroup class="w-full max-w-3xs">
<InputGroup.Input placeholder="email" aria-label="Email" />
<InputGroup.Addon align="end">@example.com</InputGroup.Addon>
</InputGroup>
<InputGroup class="w-full max-w-3xs">
<InputGroup.Addon>/api/</InputGroup.Addon>
<InputGroup.Input placeholder="endpoint" aria-label="API path" />
<InputGroup.Addon align="end">.json</InputGroup.Addon>
</InputGroup>
</div> Button
Place InputGroup.Button inside an Addon for actions that operate directly on
the input value, such as reveal/hide or clear.
<script lang="ts">
import { Eye, EyeSlash, MagnifyingGlass, X } from 'phosphor-svelte';
import { InputGroup } from 'kumo-svelte';
let show = $state(false);
let searchValue = $state('search');
</script>
<div class="flex flex-col gap-4">
<InputGroup class="w-full max-w-3xs">
<InputGroup.Input type={show ? 'text' : 'password'} value="password" aria-label="Password" />
<InputGroup.Addon align="end">
<InputGroup.Button
class="text-kumo-subtle"
icon={show ? EyeSlash : Eye}
aria-label={show ? 'Hide password' : 'Show password'}
onclick={() => (show = !show)}
/>
</InputGroup.Addon>
</InputGroup>
<InputGroup class="w-full max-w-3xs">
<InputGroup.Addon>
<MagnifyingGlass />
</InputGroup.Addon>
<InputGroup.Input bind:value={searchValue} placeholder="Search" aria-label="Search" />
{#if searchValue}
<InputGroup.Addon align="end" class="pr-1">
<InputGroup.Button aria-label="Clear search" onclick={() => (searchValue = '')}>
<X />
</InputGroup.Button>
</InputGroup.Addon>
{/if}
<InputGroup.Button variant="secondary">Search</InputGroup.Button>
</InputGroup>
</div> Button with Tooltip
Pass a tooltip prop to InputGroup.Button to show a tooltip on hover. When
no explicit aria-label is provided, the button derives it from a string
tooltip value.
<script lang="ts">
import { MagnifyingGlass, Question } from 'phosphor-svelte';
import { InputGroup } from 'kumo-svelte';
</script>
<InputGroup class="w-full max-w-2xs">
<InputGroup.Addon>
<MagnifyingGlass />
</InputGroup.Addon>
<InputGroup.Input placeholder="Search with query language..." aria-label="Search" />
<InputGroup.Addon align="end">
<InputGroup.Button
class="text-kumo-subtle"
icon={Question}
tooltip="Query language help"
/>
</InputGroup.Addon>
</InputGroup> Kbd
Place a keyboard shortcut hint inside an end Addon.
<script lang="ts">
import { MagnifyingGlass } from 'phosphor-svelte';
import { InputGroup } from 'kumo-svelte';
</script>
<InputGroup class="w-full max-w-3xs">
<InputGroup.Addon>
<MagnifyingGlass />
</InputGroup.Addon>
<InputGroup.Input placeholder="Search..." aria-label="Search" />
<InputGroup.Addon align="end">
<kbd class="border-none! bg-none!">⌘K</kbd>
</InputGroup.Addon>
</InputGroup> Loading
Place a Loader inside an end Addon as a status indicator while validating the input value.
<script lang="ts">
import { InputGroup, Loader } from 'kumo-svelte';
</script>
<InputGroup class="w-full max-w-3xs">
<InputGroup.Input value="kumo" aria-label="kumo" />
<InputGroup.Addon align="end">
<Loader />
</InputGroup.Addon>
</InputGroup> Inline Suffix
Suffix renders text that flows seamlessly next to the typed value — useful for
domain inputs like .workers.dev. Pair with a status icon Addon to show
validation state.
<script lang="ts">
import { CheckCircle, XCircle } from 'phosphor-svelte';
import { InputGroup } from 'kumo-svelte';
</script>
<div class="flex w-full max-w-2xs flex-col gap-4">
<InputGroup label="Subdomain">
<InputGroup.Input aria-label="Subdomain" value="kumo" maxlength={20} />
<InputGroup.Suffix>.workers.dev</InputGroup.Suffix>
<InputGroup.Addon align="end">
<CheckCircle weight="duotone" class="text-kumo-success" />
</InputGroup.Addon>
</InputGroup>
<InputGroup
label="Subdomain"
error={{ message: 'This subdomain is unavailable', match: true }}
>
<InputGroup.Input aria-label="Subdomain" value="kumo" maxlength={20} />
<InputGroup.Suffix>.workers.dev</InputGroup.Suffix>
<InputGroup.Addon align="end">
<XCircle weight="duotone" class="text-kumo-danger" />
</InputGroup.Addon>
</InputGroup>
</div> Sizes
Four sizes: xs, sm, base (default), and lg. The size applies to the
entire group.
<script lang="ts">
import { MagnifyingGlass, Question } from 'phosphor-svelte';
import { InputGroup } from 'kumo-svelte';
</script>
<div class="flex w-full max-w-3xs flex-col gap-4">
<InputGroup size="xs" label="Extra Small">
<InputGroup.Addon><MagnifyingGlass /></InputGroup.Addon>
<InputGroup.Input placeholder="Extra small input" />
<InputGroup.Addon align="end">
<InputGroup.Button class="text-kumo-subtle" icon={Question} shape="square" aria-label="Help" />
</InputGroup.Addon>
</InputGroup>
<InputGroup size="sm" label="Small">
<InputGroup.Addon><MagnifyingGlass /></InputGroup.Addon>
<InputGroup.Input placeholder="Small input" />
<InputGroup.Addon align="end">
<InputGroup.Button class="text-kumo-subtle" icon={Question} shape="square" aria-label="Help" />
</InputGroup.Addon>
</InputGroup>
<InputGroup label="Base (default)">
<InputGroup.Addon><MagnifyingGlass /></InputGroup.Addon>
<InputGroup.Input placeholder="Base input" />
<InputGroup.Addon align="end">
<InputGroup.Button class="text-kumo-subtle" icon={Question} shape="square" aria-label="Help" />
</InputGroup.Addon>
</InputGroup>
<InputGroup size="lg" label="Large">
<InputGroup.Addon><MagnifyingGlass /></InputGroup.Addon>
<InputGroup.Input placeholder="Large input" />
<InputGroup.Addon align="end">
<InputGroup.Button class="text-kumo-subtle" icon={Question} shape="square" aria-label="Help" />
</InputGroup.Addon>
</InputGroup>
</div> States
Various input states including error, disabled, and with description. Pass label, error, and description props directly to InputGroup.
<script lang="ts">
import { Eye, EyeSlash, MagnifyingGlass } from 'phosphor-svelte';
import { InputGroup } from 'kumo-svelte';
let show = $state(false);
</script>
<div class="flex w-full max-w-3xs flex-col gap-4">
<InputGroup
label="Error State"
error={{ message: 'Please enter a valid email address', match: true }}
>
<InputGroup.Input type="email" value="invalid-email" />
<InputGroup.Addon align="end">@example.com</InputGroup.Addon>
</InputGroup>
<InputGroup label="Disabled" disabled>
<InputGroup.Addon><MagnifyingGlass /></InputGroup.Addon>
<InputGroup.Input placeholder="Search..." />
</InputGroup>
<InputGroup label="Optional Field" required={false}>
<InputGroup.Addon>$</InputGroup.Addon>
<InputGroup.Input placeholder="0.00" />
</InputGroup>
<InputGroup
label="With Description"
description="Must be at least 8 characters"
labelTooltip="Your password is stored securely"
>
<InputGroup.Input type={show ? 'text' : 'password'} placeholder="Password" />
<InputGroup.Addon align="end">
<InputGroup.Button
class="text-kumo-subtle"
icon={show ? EyeSlash : Eye}
aria-label={show ? 'Hide password' : 'Show password'}
onclick={() => (show = !show)}
/>
</InputGroup.Addon>
</InputGroup>
</div> 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 | string | Snippet | - | Visible label content. |
| description | string | - | Supporting description text. |
| error | FieldError | - | Validation error message or matcher. |
| required | boolean | - | Marks the field as required. |
| labelTooltip | string | Snippet | - | Optional help content for the label. |
| children | Snippet | - | Child snippet rendered by the component. |
| class | string | - | Additional classes merged onto the root element. |
| id | string | - | Element id. |
| size | 'xs' | 'sm' | 'base' | 'lg' | "base" | Size preset. |
| disabled | boolean | false | Disables the component. |
| focusMode | InputGroupFocusMode | "container" | focusMode prop. |
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 | Description |
|---|---|---|---|
| value | string | number | "" | Controlled value. |
| onValueChange | (value: string) => void | - | Called when the value changes. |
| oninput | (event: Event) => void | - | Input event handler. |
| id | string | - | Element id. |
| disabled | boolean | - | Disables the component. |
InputGroup.Addon
Container for icons, text, or compact buttons positioned at the start or end of the input.
| Prop | Type | Default | Description |
|---|---|---|---|
| align | 'start' | 'end' | "start" | Floating alignment. |
| containsButton | boolean | false | containsButton prop. |
InputGroup.Button
Button for secondary actions like toggle, copy, or help. Renders inside an
Addon. Pass a tooltip prop to show a tooltip on hover.
| Prop | Type | Default | Description |
|---|---|---|---|
| tooltip | string | - | Tooltip content. |
| tooltipSide | string | - | tooltipSide prop. |
| variant | 'primary' | 'secondary' | 'ghost' | 'destructive' | 'secondary-destructive' | 'outline' | "ghost" | Visual variant. |
| size | 'xs' | 'sm' | 'base' | 'lg' | - | Size preset. |
| icon | Component | - | Icon rendered by the component. |
| shape | 'base' | 'square' | 'circle' | - | Shape preset. |
| disabled | boolean | - | Disables the component. |
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 | Description |
|---|---|---|---|
| No component-specific props. Accepts standard HTML attributes. | |||
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, which renders a visible label with built-in Field supportaria-labelon InputGroup.Input for inputs without a visible labelaria-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.