<script lang="ts">
import CheckCircleIcon from "phosphor-svelte/lib/CheckCircleIcon";
import { Loader } from "kumo-svelte/components/loader";
import * as InputGroup from "kumo-svelte/components/input-group";
let value = $state("kumo");
let status = $state<"idle" | "loading" | "success">("success");
let timer: ReturnType<typeof setTimeout> | undefined;
function handleValueChange(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.Root>
<InputGroup.Input maxlength={20} {value} onValueChange={handleValueChange} aria-label="Worker subdomain" />
<InputGroup.Suffix>.workers.dev</InputGroup.Suffix>
{#if status !== "idle"}
<InputGroup.Addon align="end">
{#if status === "loading"}
<Loader />
{:else}
<CheckCircleIcon weight="duotone" class="text-kumo-success" />
{/if}
</InputGroup.Addon>
{/if}
</InputGroup.Root>
</div>
Import
import * as InputGroup 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 MagnifyingGlassIcon from "phosphor-svelte/lib/MagnifyingGlassIcon";
import * as InputGroup from "kumo-svelte/components/input-group";
</script>
<InputGroup.Root label="Search" description="Find pages, components, and more">
<InputGroup.Addon>
<MagnifyingGlassIcon />
</InputGroup.Addon>
<InputGroup.Input placeholder="Search..." />
</InputGroup.Root> Bare InputGroup (Custom Layouts)
For custom form layouts, use InputGroup without label. Provide aria-label on InputGroup.Input for accessibility.
<InputGroup.Root>
<InputGroup.Addon>
<MagnifyingGlassIcon />
</InputGroup.Addon>
<InputGroup.Input placeholder="Search..." aria-label="Search" />
</InputGroup.Root> 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/lib/LinkIcon";
import * as InputGroup from "kumo-svelte/components/input-group";
</script>
<InputGroup.Root class="w-full max-w-3xs">
<InputGroup.Addon>
<LinkIcon />
</InputGroup.Addon>
<InputGroup.Input placeholder="Paste a link..." aria-label="Link" />
</InputGroup.Root>
Text
Use Addon to place text prefixes or suffixes alongside the input.
<script lang="ts">
import * as InputGroup from "kumo-svelte/components/input-group";
</script>
<div class="flex flex-col gap-4">
<InputGroup.Root class="w-full max-w-3xs">
<InputGroup.Addon>@</InputGroup.Addon>
<InputGroup.Input placeholder="username" aria-label="Username" />
</InputGroup.Root>
<InputGroup.Root class="w-full max-w-3xs">
<InputGroup.Input placeholder="email" aria-label="Email" />
<InputGroup.Addon align="end">@example.com</InputGroup.Addon>
</InputGroup.Root>
<InputGroup.Root 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.Root>
</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 EyeIcon from "phosphor-svelte/lib/EyeIcon";
import EyeSlashIcon from "phosphor-svelte/lib/EyeSlashIcon";
import MagnifyingGlassIcon from "phosphor-svelte/lib/MagnifyingGlassIcon";
import XIcon from "phosphor-svelte/lib/XIcon";
import * as InputGroup from "kumo-svelte/components/input-group";
let show = $state(false);
let searchValue = $state("search");
</script>
<div class="flex flex-col gap-4">
<InputGroup.Root class="w-full max-w-3xs">
<InputGroup.Input type={show ? "text" : "password"} value="password" aria-label="Password" />
<InputGroup.Addon align="end" containsButton>
<InputGroup.Button
class="text-kumo-subtle"
aria-label={show ? "Hide password" : "Show password"}
onclick={() => {
show = !show;
}}
>
{#if show}
<EyeSlashIcon />
{:else}
<EyeIcon />
{/if}
</InputGroup.Button>
</InputGroup.Addon>
</InputGroup.Root>
<InputGroup.Root class="w-full max-w-3xs" focusMode="individual">
<InputGroup.Addon>
<MagnifyingGlassIcon />
</InputGroup.Addon>
<InputGroup.Input bind:value={searchValue} placeholder="Search" aria-label="Search" />
{#if searchValue}
<InputGroup.Addon align="end" containsButton class="pr-1">
<InputGroup.Button
aria-label="Clear search"
onclick={() => {
searchValue = "";
}}
>
<XIcon />
</InputGroup.Button>
</InputGroup.Addon>
{/if}
<InputGroup.Button variant="secondary">Search</InputGroup.Button>
</InputGroup.Root>
</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 MagnifyingGlassIcon from "phosphor-svelte/lib/MagnifyingGlassIcon";
import QuestionIcon from "phosphor-svelte/lib/QuestionIcon";
import * as InputGroup from "kumo-svelte/components/input-group";
</script>
<InputGroup.Root class="w-full max-w-2xs">
<InputGroup.Addon>
<MagnifyingGlassIcon />
</InputGroup.Addon>
<InputGroup.Input placeholder="Search with query language..." aria-label="Search" />
<InputGroup.Addon align="end" containsButton>
<InputGroup.Button class="text-kumo-subtle" tooltip="Query language help">
<QuestionIcon />
</InputGroup.Button>
</InputGroup.Addon>
</InputGroup.Root>
Kbd
Place a keyboard shortcut hint inside an end Addon.
<script lang="ts">
import MagnifyingGlassIcon from "phosphor-svelte/lib/MagnifyingGlassIcon";
import * as InputGroup from "kumo-svelte/components/input-group";
</script>
<InputGroup.Root class="w-full max-w-3xs">
<InputGroup.Addon>
<MagnifyingGlassIcon />
</InputGroup.Addon>
<InputGroup.Input placeholder="Search..." aria-label="Search" />
<InputGroup.Addon align="end">
<kbd class="border-none! bg-none!">⌘K</kbd>
</InputGroup.Addon>
</InputGroup.Root>
Loading
Place a Loader inside an end Addon as a status indicator while validating the input value.
<script lang="ts">
import { Loader } from "kumo-svelte/components/loader";
import * as InputGroup from "kumo-svelte/components/input-group";
</script>
<InputGroup.Root class="w-full max-w-3xs">
<InputGroup.Input value="kumo" aria-label="kumo" />
<InputGroup.Addon align="end">
<Loader />
</InputGroup.Addon>
</InputGroup.Root>
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.
This subdomain is unavailable
<script lang="ts">
import CheckCircleIcon from "phosphor-svelte/lib/CheckCircleIcon";
import XCircleIcon from "phosphor-svelte/lib/XCircleIcon";
import * as InputGroup from "kumo-svelte/components/input-group";
</script>
<div class="flex w-full max-w-2xs flex-col gap-4">
<InputGroup.Root label="Subdomain">
<InputGroup.Input aria-label="Subdomain" value="kumo" maxlength={20} />
<InputGroup.Suffix>.workers.dev</InputGroup.Suffix>
<InputGroup.Addon align="end">
<CheckCircleIcon weight="duotone" class="text-kumo-success" />
</InputGroup.Addon>
</InputGroup.Root>
<InputGroup.Root label="Subdomain" error="This subdomain is unavailable">
<InputGroup.Input aria-label="Subdomain" value="kumo" maxlength={20} />
<InputGroup.Suffix>.workers.dev</InputGroup.Suffix>
<InputGroup.Addon align="end">
<XCircleIcon weight="duotone" class="text-kumo-danger" />
</InputGroup.Addon>
</InputGroup.Root>
</div>
Sizes
Four sizes: xs, sm, base (default), and lg. The size applies to the entire group.
<script lang="ts">
import MagnifyingGlassIcon from "phosphor-svelte/lib/MagnifyingGlassIcon";
import QuestionIcon from "phosphor-svelte/lib/QuestionIcon";
import * as InputGroup from "kumo-svelte/components/input-group";
const sizes = [
["xs", "Extra Small", "Extra small input"],
["sm", "Small", "Small input"],
["base", "Base (default)", "Base input"],
["lg", "Large", "Large input"],
] as const;
</script>
<div class="flex w-full max-w-3xs flex-col gap-4">
{#each sizes as [size, label, placeholder]}
<InputGroup.Root {size} {label}>
<InputGroup.Addon>
<MagnifyingGlassIcon />
</InputGroup.Addon>
<InputGroup.Input {placeholder} />
<InputGroup.Addon align="end" containsButton>
<InputGroup.Button class="text-kumo-subtle" shape="square" aria-label="Help">
<QuestionIcon />
</InputGroup.Button>
</InputGroup.Addon>
</InputGroup.Root>
{/each}
</div>
States
Various input states including error, disabled, and with description. Pass label, error, and description props directly to InputGroup.
Please enter a valid email address
Must be at least 8 characters
<script lang="ts">
import EyeIcon from "phosphor-svelte/lib/EyeIcon";
import EyeSlashIcon from "phosphor-svelte/lib/EyeSlashIcon";
import MagnifyingGlassIcon from "phosphor-svelte/lib/MagnifyingGlassIcon";
import * as InputGroup from "kumo-svelte/components/input-group";
let show = $state(false);
</script>
<div class="flex w-full max-w-3xs flex-col gap-4">
<InputGroup.Root label="Error State" error="Please enter a valid email address">
<InputGroup.Input type="email" value="invalid-email" />
<InputGroup.Addon align="end">@example.com</InputGroup.Addon>
</InputGroup.Root>
<InputGroup.Root label="Disabled" disabled>
<InputGroup.Addon>
<MagnifyingGlassIcon />
</InputGroup.Addon>
<InputGroup.Input placeholder="Search..." />
</InputGroup.Root>
<InputGroup.Root label="Optional Field" required={false}>
<InputGroup.Addon>$</InputGroup.Addon>
<InputGroup.Input placeholder="0.00" />
</InputGroup.Root>
<InputGroup.Root label="With Description" description="Must be at least 8 characters">
<InputGroup.Input type={show ? "text" : "password"} placeholder="Password" />
<InputGroup.Addon align="end" containsButton>
<InputGroup.Button
class="text-kumo-subtle"
aria-label={show ? "Hide password" : "Show password"}
onclick={() => {
show = !show;
}}
>
{#if show}
<EyeSlashIcon />
{:else}
<EyeIcon />
{/if}
</InputGroup.Button>
</InputGroup.Addon>
</InputGroup.Root>
</div>
API Reference
InputGroup
| Prop | Type | Default |
|---|---|---|
| children* | Snippet | — |
| class | string | — |
| description | Snippet | string | — |
| disabled | boolean | false |
| error | string | { message: Snippet | string; match: FieldErrorMatch } | — |
| focusMode | InputGroupFocusMode | "container" |
| label | Snippet | string | — |
| labelTooltip | Snippet | — |
| required | boolean | — |
| size | KumoInputSize | KUMO_INPUT_GROUP_DEFAULT_VARIANTS.size |
InputGroup.Addon
| Prop | Type | Default |
|---|---|---|
| align | "start" | "end" | "start" |
| children | Snippet | — |
| class | string | — |
| containsButton | boolean | false |
InputGroup.Button
| Prop | Type | Default |
|---|---|---|
| children | Snippet | — |
| tooltip | Snippet | string | — |
| tooltipSide | KumoTooltipSide | "bottom" |
InputGroup.Input
| Prop | Type | Default |
|---|---|---|
| class | string | — |
| onValueChange | (value: string) => void | — |
| value | string | number | string[] | undefined | bindable |
InputGroup.Suffix
| Prop | Type | Default |
|---|---|---|
| children | Snippet | — |
| class | string | — |
Accessibility
Label Requirement
InputGroup requires an accessible name via one of:
labelprop on InputGroup, which renders a visible label with built-in Field supportaria-labelonInputGroup.Inputfor inputs without a visible labelaria-labelledbyonInputGroup.Inputfor custom label association
Group Semantics
InputGroup groups the input with its addons visually. Keep addon text short and ensure action buttons have a label.