InputGroup
kumo-svelte
<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

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.

.workers.dev
.workers.dev
This subdomain is unavailable
<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.

@example.com
Please enter a valid email address
$
Must be at least 8 characters
<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.

PropTypeDefaultDescription
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 booleanfalseDisables 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.

PropTypeDefaultDescription
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.

PropTypeDefaultDescription
align 'start' | 'end'"start"Floating alignment.
containsButton booleanfalsecontainsButton 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.

PropTypeDefaultDescription
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.

PropTypeDefaultDescription
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:

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, which 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.