Kumo Svelte
  • Home
  • Installation
  • Colors
  • Accessibility
  • Changelog
  • Autocomplete
  • Badge
  • Banner
  • Breadcrumbs
  • Button
  • Checkbox
  • Clipboard Text
  • Cloudflare Logo
  • CodeHighlighted
  • Collapsible
  • Combobox
  • Command Palette
  • Date Picker
  • Dialog
  • Dropdown
  • Empty
  • Flow
  • Grid
  • Input
  • InputArea
  • InputGroup
  • Label
  • Layer Card
  • Link
  • Loader
  • MenuBar
  • Meter
  • Pagination
  • Popover
  • Radio
  • Select
  • Sensitive Input
  • Sidebar
  • Skeleton Line
  • Switch
  • Table
  • Table of Contents
  • Tabs
  • Text
  • Toast
  • Toolbar
  • Tooltip
  • Charts
  • Colors
  • Timeseries
  • Maps
  • Sankey
  • Custom Chart
  • Page Header
  • Resource List
  • Delete Resource
InputGroup
kumo-svelte

InputGroup

Compose inputs with addons, icons, buttons, and text for rich form fields.

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

.workers.dev
.workers.dev

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.

@example.com

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

PropTypeDefault
children*Snippet—
classstring—
descriptionSnippet | string—
disabledbooleanfalse
errorstring | { message: Snippet | string; match: FieldErrorMatch }—
focusModeInputGroupFocusMode"container"
labelSnippet | string—
labelTooltipSnippet—
requiredboolean—
sizeKumoInputSizeKUMO_INPUT_GROUP_DEFAULT_VARIANTS.size

InputGroup.Addon

PropTypeDefault
align"start" | "end""start"
childrenSnippet—
classstring—
containsButtonbooleanfalse

InputGroup.Button

PropTypeDefault
childrenSnippet—
tooltipSnippet | string—
tooltipSideKumoTooltipSide"bottom"

InputGroup.Input

PropTypeDefault
classstring—
onValueChange(value: string) => void—
valuestring | number | string[] | undefinedbindable

InputGroup.Suffix

PropTypeDefault
childrenSnippet—
classstring—

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

Group Semantics

InputGroup groups the input with its addons visually. Keep addon text short and ensure action buttons have a label.

On this page

  • Import
  • Usage
    • With Built-in Field (Recommended)
    • Bare InputGroup (Custom Layouts)
  • Examples
    • Icon
    • Text
    • Button
    • Button with Tooltip
    • Kbd
    • Loading
    • Inline Suffix
    • Sizes
    • States
  • API Reference
    • InputGroup
    • InputGroup.Addon
    • InputGroup.Button
    • InputGroup.Input
    • InputGroup.Suffix
  • Accessibility
    • Label Requirement
    • Group Semantics