8.0k New

Input Group

Add addons, buttons, and helper content to inputs.

Epicenter

Local-first, open source apps

Special Sponsor
12 results
<script lang="ts">
  import SearchIcon from "@lucide/svelte/icons/search";
  import * as InputGroup from "$lib/components/ui/input-group/index.js";
</script>
 
<InputGroup.Root class="max-w-xs">
  <InputGroup.Input placeholder="Search..." />
  <InputGroup.Addon>
    <SearchIcon />
  </InputGroup.Addon>
  <InputGroup.Addon align="inline-end">12 results</InputGroup.Addon>
</InputGroup.Root>

Installation

pnpm dlx shadcn-svelte@latest add input-group

Usage

<script lang="ts">
  import * as InputGroup from "$lib/components/ui/input-group/index.js";
  import SearchIcon from "@lucide/svelte/icons/search";
</script>
<InputGroup.Root>
  <InputGroup.Input placeholder="Search..." />
  <InputGroup.Addon>
    <SearchIcon />
  </InputGroup.Addon>
  <InputGroup.Addon align="inline-end">
    <InputGroup.Button>Search</InputGroup.Button>
  </InputGroup.Addon>
</InputGroup.Root>

Align

Use the align prop on InputGroup.Addon to position the addon relative to the input.

inline-start

Use align="inline-start" to position the addon at the start of the input. This is the default.

Icon positioned at the start.

<script lang="ts">
  import SearchIcon from "@lucide/svelte/icons/search";
  import * as Field from "$lib/components/ui/field/index.js";
  import * as InputGroup from "$lib/components/ui/input-group/index.js";
</script>
 
<Field.Field class="max-w-sm">
  <Field.Label for="inline-start-input">Input</Field.Label>
  <InputGroup.Root>
    <InputGroup.Input id="inline-start-input" placeholder="Search..." />
    <InputGroup.Addon align="inline-start">
      <SearchIcon class="text-muted-foreground" />
    </InputGroup.Addon>
  </InputGroup.Root>
  <Field.Description>Icon positioned at the start.</Field.Description>
</Field.Field>

inline-end

Use align="inline-end" to position the addon at the end of the input.

Icon positioned at the end.

<script lang="ts">
  import EyeOffIcon from "@lucide/svelte/icons/eye-off";
  import * as Field from "$lib/components/ui/field/index.js";
  import * as InputGroup from "$lib/components/ui/input-group/index.js";
</script>
 
<Field.Field class="max-w-sm">
  <Field.Label for="inline-end-input">Input</Field.Label>
  <InputGroup.Root>
    <InputGroup.Input
      id="inline-end-input"
      type="password"
      placeholder="Enter password"
    />
    <InputGroup.Addon align="inline-end">
      <EyeOffIcon />
    </InputGroup.Addon>
  </InputGroup.Root>
  <Field.Description>Icon positioned at the end.</Field.Description>
</Field.Field>

block-start

Use align="block-start" to position the addon above the input.

Full Name

Header positioned above the input.

script.js

Header positioned above the textarea.

<script lang="ts">
  import CopyIcon from "@lucide/svelte/icons/copy";
  import FileCodeIcon from "@lucide/svelte/icons/file-code";
  import * as Field from "$lib/components/ui/field/index.js";
  import * as InputGroup from "$lib/components/ui/input-group/index.js";
</script>
 
<Field.Group class="max-w-sm">
  <Field.Field>
    <Field.Label for="block-start-input">Input</Field.Label>
    <InputGroup.Root class="h-auto">
      <InputGroup.Input id="block-start-input" placeholder="Enter your name" />
      <InputGroup.Addon align="block-start">
        <InputGroup.Text>Full Name</InputGroup.Text>
      </InputGroup.Addon>
    </InputGroup.Root>
    <Field.Description>Header positioned above the input.</Field.Description>
  </Field.Field>
  <Field.Field>
    <Field.Label for="block-start-textarea">Textarea</Field.Label>
    <InputGroup.Root>
      <InputGroup.Textarea
        id="block-start-textarea"
        placeholder="console.log('Hello, world!');"
        class="font-mono text-sm"
      />
      <InputGroup.Addon align="block-start">
        <FileCodeIcon class="text-muted-foreground" />
        <InputGroup.Text class="font-mono">script.js</InputGroup.Text>
        <InputGroup.Button size="icon-xs" class="ml-auto">
          <CopyIcon />
          <span class="sr-only">Copy</span>
        </InputGroup.Button>
      </InputGroup.Addon>
    </InputGroup.Root>
    <Field.Description>Header positioned above the textarea.</Field.Description>
  </Field.Field>
</Field.Group>

block-end

Use align="block-end" to position the addon below the input.

USD

Footer positioned below the input.

0/280

Footer positioned below the textarea.

<script lang="ts">
  import * as Field from "$lib/components/ui/field/index.js";
  import * as InputGroup from "$lib/components/ui/input-group/index.js";
</script>
 
<Field.Group class="max-w-sm">
  <Field.Field>
    <Field.Label for="block-end-input">Input</Field.Label>
    <InputGroup.Root class="h-auto">
      <InputGroup.Input id="block-end-input" placeholder="Enter amount" />
      <InputGroup.Addon align="block-end">
        <InputGroup.Text>USD</InputGroup.Text>
      </InputGroup.Addon>
    </InputGroup.Root>
    <Field.Description>Footer positioned below the input.</Field.Description>
  </Field.Field>
  <Field.Field>
    <Field.Label for="block-end-textarea">Textarea</Field.Label>
    <InputGroup.Root>
      <InputGroup.Textarea
        id="block-end-textarea"
        placeholder="Write a comment..."
      />
      <InputGroup.Addon align="block-end">
        <InputGroup.Text>0/280</InputGroup.Text>
        <InputGroup.Button variant="default" size="sm" class="ml-auto"
          >Post</InputGroup.Button
        >
      </InputGroup.Addon>
    </InputGroup.Root>
    <Field.Description>Footer positioned below the textarea.</Field.Description>
  </Field.Field>
</Field.Group>

Icon

<script lang="ts">
  import CheckIcon from "@lucide/svelte/icons/check";
  import CreditCardIcon from "@lucide/svelte/icons/credit-card";
  import InfoIcon from "@lucide/svelte/icons/info";
  import MailIcon from "@lucide/svelte/icons/mail";
  import SearchIcon from "@lucide/svelte/icons/search";
  import StarIcon from "@lucide/svelte/icons/star";
  import * as InputGroup from "$lib/components/ui/input-group/index.js";
</script>
 
<div class="grid w-full max-w-sm gap-6">
  <InputGroup.Root>
    <InputGroup.Input placeholder="Search..." />
    <InputGroup.Addon>
      <SearchIcon />
    </InputGroup.Addon>
  </InputGroup.Root>
  <InputGroup.Root>
    <InputGroup.Input type="email" placeholder="Enter your email" />
    <InputGroup.Addon>
      <MailIcon />
    </InputGroup.Addon>
  </InputGroup.Root>
  <InputGroup.Root>
    <InputGroup.Input placeholder="Card number" />
    <InputGroup.Addon>
      <CreditCardIcon />
    </InputGroup.Addon>
    <InputGroup.Addon align="inline-end">
      <CheckIcon />
    </InputGroup.Addon>
  </InputGroup.Root>
  <InputGroup.Root>
    <InputGroup.Input placeholder="Card number" />
    <InputGroup.Addon align="inline-end">
      <StarIcon />
      <InfoIcon />
    </InputGroup.Addon>
  </InputGroup.Root>
</div>

Text

$
USD
https://
.com
@company.com
120 characters left
<script lang="ts">
  import * as InputGroup from "$lib/components/ui/input-group/index.js";
</script>
 
<div class="grid w-full max-w-sm gap-6">
  <InputGroup.Root>
    <InputGroup.Addon>
      <InputGroup.Text>$</InputGroup.Text>
    </InputGroup.Addon>
    <InputGroup.Input placeholder="0.00" />
    <InputGroup.Addon align="inline-end">
      <InputGroup.Text>USD</InputGroup.Text>
    </InputGroup.Addon>
  </InputGroup.Root>
  <InputGroup.Root>
    <InputGroup.Addon>
      <InputGroup.Text>https://</InputGroup.Text>
    </InputGroup.Addon>
    <InputGroup.Input placeholder="example.com" class="!ps-0.5" />
    <InputGroup.Addon align="inline-end">
      <InputGroup.Text>.com</InputGroup.Text>
    </InputGroup.Addon>
  </InputGroup.Root>
  <InputGroup.Root>
    <InputGroup.Input placeholder="Enter your username" />
    <InputGroup.Addon align="inline-end">
      <InputGroup.Text>@company.com</InputGroup.Text>
    </InputGroup.Addon>
  </InputGroup.Root>
  <InputGroup.Root>
    <InputGroup.Textarea placeholder="Enter your message" />
    <InputGroup.Addon align="block-end">
      <InputGroup.Text class="text-xs text-muted-foreground"
        >120 characters left</InputGroup.Text
      >
    </InputGroup.Addon>
  </InputGroup.Root>
</div>

Button

https://
<script lang="ts">
  import IconCheck from "@tabler/icons-svelte/icons/check";
  import IconCopy from "@tabler/icons-svelte/icons/copy";
  import IconInfoCircle from "@tabler/icons-svelte/icons/info-circle";
  import IconStar from "@tabler/icons-svelte/icons/star";
  import * as InputGroup from "$lib/components/ui/input-group/index.js";
  import * as Popover from "$lib/components/ui/popover/index.js";
  import { UseClipboard } from "$lib/hooks/use-clipboard.svelte.js";
 
  let isFavorite = $state(false);
 
  const clipboard = new UseClipboard();
</script>
 
<div class="grid w-full max-w-sm gap-6">
  <InputGroup.Root>
    <InputGroup.Input placeholder="https://x.com/shadcn" readonly />
    <InputGroup.Addon align="inline-end">
      <InputGroup.Button
        aria-label="Copy"
        title="Copy"
        size="icon-xs"
        onclick={() => clipboard.copy("https://x.com/shadcn")}
      >
        {#if clipboard.copied}
          <IconCheck />
        {:else}
          <IconCopy />
        {/if}
      </InputGroup.Button>
    </InputGroup.Addon>
  </InputGroup.Root>
  <InputGroup.Root class="[--radius:9999px]">
    <Popover.Root>
      <Popover.Trigger>
        {#snippet child({ props })}
          <InputGroup.Addon>
            <InputGroup.Button {...props} variant="secondary" size="icon-xs">
              <IconInfoCircle />
            </InputGroup.Button>
          </InputGroup.Addon>
        {/snippet}
      </Popover.Trigger>
      <Popover.Content
        align="start"
        class="flex flex-col gap-1 rounded-xl text-sm"
      >
        <p class="font-medium">Your connection is not secure.</p>
        <p>You should not enter any sensitive information on this site.</p>
      </Popover.Content>
    </Popover.Root>
    <InputGroup.Addon class="ps-1.5 text-muted-foreground"
      >https://</InputGroup.Addon
    >
    <InputGroup.Input id="input-secure-19" />
    <InputGroup.Addon align="inline-end">
      <InputGroup.Button
        onclick={() => (isFavorite = !isFavorite)}
        size="icon-xs"
      >
        <IconStar
          data-favorite={isFavorite}
          class="data-[favorite=true]:fill-blue-600 data-[favorite=true]:stroke-blue-600"
        />
      </InputGroup.Button>
    </InputGroup.Addon>
  </InputGroup.Root>
  <InputGroup.Root>
    <InputGroup.Input placeholder="Type to search..." />
    <InputGroup.Addon align="inline-end">
      <InputGroup.Button variant="secondary">Search</InputGroup.Button>
    </InputGroup.Addon>
  </InputGroup.Root>
</div>

Kbd

⌘K
<script lang="ts">
  import SearchIcon from "@lucide/svelte/icons/search";
  import * as InputGroup from "$lib/components/ui/input-group/index.js";
  import { Kbd } from "$lib/components/ui/kbd/index.js";
</script>
 
<InputGroup.Root class="max-w-sm">
  <InputGroup.Input placeholder="Search..." />
  <InputGroup.Addon>
    <SearchIcon class="text-muted-foreground" />
  </InputGroup.Addon>
  <InputGroup.Addon align="inline-end">
    <Kbd>⌘K</Kbd>
  </InputGroup.Addon>
</InputGroup.Root>
<script lang="ts">
  import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
  import MoreHorizontalIcon from "@lucide/svelte/icons/more-horizontal";
  import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
  import * as InputGroup from "$lib/components/ui/input-group/index.js";
</script>
 
<div class="grid w-full max-w-sm gap-4">
  <InputGroup.Root>
    <InputGroup.Input placeholder="Enter file name" />
    <InputGroup.Addon align="inline-end">
      <DropdownMenu.Root>
        <DropdownMenu.Trigger>
          {#snippet child({ props })}
            <InputGroup.Button
              {...props}
              variant="ghost"
              aria-label="More"
              size="icon-xs"
            >
              <MoreHorizontalIcon />
            </InputGroup.Button>
          {/snippet}
        </DropdownMenu.Trigger>
        <DropdownMenu.Content align="end" sideOffset={8} alignOffset={-4}>
          <DropdownMenu.Group>
            <DropdownMenu.Item>Settings</DropdownMenu.Item>
            <DropdownMenu.Item>Copy path</DropdownMenu.Item>
            <DropdownMenu.Item>Open location</DropdownMenu.Item>
          </DropdownMenu.Group>
        </DropdownMenu.Content>
      </DropdownMenu.Root>
    </InputGroup.Addon>
  </InputGroup.Root>
  <InputGroup.Root>
    <InputGroup.Input placeholder="Enter search query" />
    <InputGroup.Addon align="inline-end">
      <DropdownMenu.Root>
        <DropdownMenu.Trigger>
          {#snippet child({ props })}
            <InputGroup.Button
              {...props}
              variant="ghost"
              class="!pe-1.5 text-xs"
            >
              Search In... <ChevronDownIcon class="size-3" />
            </InputGroup.Button>
          {/snippet}
        </DropdownMenu.Trigger>
        <DropdownMenu.Content align="end" sideOffset={8} alignOffset={-4}>
          <DropdownMenu.Group>
            <DropdownMenu.Item>Documentation</DropdownMenu.Item>
            <DropdownMenu.Item>Blog Posts</DropdownMenu.Item>
            <DropdownMenu.Item>Changelog</DropdownMenu.Item>
          </DropdownMenu.Group>
        </DropdownMenu.Content>
      </DropdownMenu.Root>
    </InputGroup.Addon>
  </InputGroup.Root>
</div>

Spinner

Saving...
Please wait...
<script lang="ts">
  import LoaderIcon from "@lucide/svelte/icons/loader";
  import * as InputGroup from "$lib/components/ui/input-group/index.js";
  import { Spinner } from "$lib/components/ui/spinner/index.js";
</script>
 
<div class="grid w-full max-w-sm gap-4">
  <InputGroup.Root>
    <InputGroup.Input placeholder="Searching..." />
    <InputGroup.Addon align="inline-end">
      <Spinner />
    </InputGroup.Addon>
  </InputGroup.Root>
  <InputGroup.Root>
    <InputGroup.Input placeholder="Processing..." />
    <InputGroup.Addon>
      <Spinner />
    </InputGroup.Addon>
  </InputGroup.Root>
  <InputGroup.Root>
    <InputGroup.Input placeholder="Saving changes..." />
    <InputGroup.Addon align="inline-end">
      <InputGroup.Text>Saving...</InputGroup.Text>
      <Spinner />
    </InputGroup.Addon>
  </InputGroup.Root>
  <InputGroup.Root>
    <InputGroup.Input placeholder="Refreshing data..." />
    <InputGroup.Addon>
      <LoaderIcon class="animate-spin" />
    </InputGroup.Addon>
    <InputGroup.Addon align="inline-end">
      <InputGroup.Text class="text-muted-foreground"
        >Please wait...</InputGroup.Text
      >
    </InputGroup.Addon>
  </InputGroup.Root>
</div>

Textarea

Line 1, Column 1
script.js
<script lang="ts">
  import IconBrandJavascript from "@tabler/icons-svelte/icons/brand-javascript";
  import IconCopy from "@tabler/icons-svelte/icons/copy";
  import IconCornerDownLeft from "@tabler/icons-svelte/icons/corner-down-left";
  import IconRefresh from "@tabler/icons-svelte/icons/refresh";
  import * as InputGroup from "$lib/components/ui/input-group/index.js";
</script>
 
<div class="grid w-full max-w-md gap-4">
  <InputGroup.Root>
    <InputGroup.Textarea
      id="textarea-code-32"
      placeholder="console.log('Hello, world!');"
      class="min-h-[200px]"
    />
    <InputGroup.Addon align="block-end" class="border-t">
      <InputGroup.Text>Line 1, Column 1</InputGroup.Text>
      <InputGroup.Button size="sm" class="ms-auto" variant="default">
        Run <IconCornerDownLeft />
      </InputGroup.Button>
    </InputGroup.Addon>
    <InputGroup.Addon align="block-start" class="border-b">
      <InputGroup.Text class="font-mono font-medium">
        <IconBrandJavascript />
        script.js
      </InputGroup.Text>
      <InputGroup.Button class="ms-auto" size="icon-xs">
        <IconRefresh />
      </InputGroup.Button>
      <InputGroup.Button variant="ghost" size="icon-xs">
        <IconCopy />
      </InputGroup.Button>
    </InputGroup.Addon>
  </InputGroup.Root>
</div>

Custom Input

Add the data-slot="input-group-control" attribute to your custom input for automatic focus state handling.

Here's an example of a custom auto-resizing textarea.

<script lang="ts">
  import * as InputGroup from "$lib/components/ui/input-group/index.js";
</script>
 
<div class="grid w-full max-w-sm gap-6">
  <InputGroup.Root>
    <textarea
      data-slot="input-group-control"
      class="flex field-sizing-content min-h-16 w-full resize-none rounded-md bg-transparent px-3 py-2.5 text-base transition-[color,box-shadow] outline-none md:text-sm"
      placeholder="Autoresize textarea..."></textarea>
    <InputGroup.Addon align="block-end">
      <InputGroup.Button class="ms-auto" size="sm" variant="default"
        >Submit</InputGroup.Button
      >
    </InputGroup.Addon>
  </InputGroup.Root>
</div>