Skip to content
edmi-ui

Input Group

Icons, text, buttons or keys around an input sharing one focus ring.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
USDC
https://
52%
USDC
https://
52%
USDC
https://
52%
Terminal window
npx shadcn@latest add @edmi-ui/input-group
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/input-group
Terminal window
yarn dlx shadcn@latest add @edmi-ui/input-group
Terminal window
bunx shadcn@latest add @edmi-ui/input-group

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/input-group
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/input-group
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/input-group
Terminal window
bunx shadcn-vue@latest add @edmi-ui/input-group

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/input-group.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/input-group.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/input-group.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/input-group.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
} from "@/components/ui/input-group";
<div className="grid w-full max-w-sm gap-3">
<InputGroup>
<InputGroupAddon>
<MagnifyingGlassIcon />
</InputGroupAddon>
<InputGroupInput placeholder="Search indexes" />
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="1,000.00" />
<InputGroupAddon align="inline-end">
<InputGroupText>USDC</InputGroupText>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupAddon>
<InputGroupText>https://</InputGroupText>
</InputGroupAddon>
<InputGroupInput defaultValue="stockbreak.fun/mag4" />
</InputGroup>
{/* … */}
</div>
<script setup lang="ts">
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
} from "@/components/ui/input-group";
</script>
<template>
<div class="grid w-full max-w-sm gap-3">
<InputGroup>
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
<InputGroupInput placeholder="Search indexes" />
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="1,000.00" />
<InputGroupAddon align="inline-end">
<InputGroupText>USDC</InputGroupText>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupAddon>
<InputGroupText>https://</InputGroupText>
</InputGroupAddon>
<InputGroupInput default-value="stockbreak.fun/mag4" />
</InputGroup>
<!-- … -->
</div>
</template>
<script lang="ts">
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
} from "$lib/components/ui/input-group";
</script>
<div class="grid w-full max-w-sm gap-3">
<InputGroup>
<InputGroupAddon>
<MagnifyingGlassIcon />
</InputGroupAddon>
<InputGroupInput placeholder="Search indexes" />
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="1,000.00" />
<InputGroupAddon align="inline-end">
<InputGroupText>USDC</InputGroupText>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupAddon>
<InputGroupText>https://</InputGroupText>
</InputGroupAddon>
<InputGroupInput value="stockbreak.fun/mag4" />
</InputGroup>
<!-- … -->
</div>
registry/ui/input-group.tsx
"use client";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "cn";
import type * as React from "react";
import { Button } from "@/registry/edmi/ui/button";
import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation";
import { Input } from "@/registry/edmi/ui/input";
import { Textarea } from "@/registry/edmi/ui/textarea";
// ✦ depth (v4): the group is the field; focus-within swaps the edge for the ring
const groupElevation = {
sunken:
"border-sk-bd bg-sk-bg shadow-sunken has-[[data-slot=input-group-control]:focus-visible]:bg-card [&_[data-slot=input-group-addon]]:bg-transparent! [&_[data-slot=input-group-text]]:bg-transparent!",
flat: "",
raised:
"border-transparent bg-[image:linear-gradient(var(--bv-face-b),var(--bv-face-b))] shadow-raised has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:shadow-ring [&_[data-slot=input-group-addon]]:bg-transparent! [&_[data-slot=input-group-text]]:bg-transparent!",
floating:
"border-transparent bg-[image:linear-gradient(var(--bv-face-b),var(--bv-face-b))] shadow-floating has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:shadow-ring [&_[data-slot=input-group-addon]]:bg-transparent! [&_[data-slot=input-group-text]]:bg-transparent!",
};
function InputGroup({
className,
elevation,
...props
}: React.ComponentProps<"div"> & {
/** ✦ depth: sunken -1, flat 0, raised +1, floating +2. */
elevation?: Elevation;
}) {
const level = useElevation(elevation, "field");
return (
// biome-ignore lint/a11y/useSemanticElements: stock markup; div keeps layout predictable
<div
data-slot="input-group"
role="group"
className={cn(
"group/input-group relative flex h-9 w-full min-w-0 items-stretch overflow-hidden rounded-md border border-input bg-card outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:shadow-none has-disabled:bg-muted has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:shadow-ring has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:shadow-ring-error has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3",
groupElevation[level],
className,
)}
{...props}
/>
);
}
const inputGroupAddonVariants = cva(
"flex h-auto cursor-text items-center justify-center gap-2 text-[13px] font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 has-[>[data-slot=input-group-text]]:bg-muted has-[>[data-slot=input-group-text]]:px-0 [&>kbd]:rounded-[5px] [&>svg:not([class*='size-'])]:size-4",
{
variants: {
align: {
"inline-start":
"order-first pl-3 has-[>[data-slot=input-group-text]]:border-r has-[>[data-slot=input-group-text]]:border-input has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
"inline-end":
"order-last pr-2.5 has-[>[data-slot=input-group-text]]:border-l has-[>[data-slot=input-group-text]]:border-input has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
"block-start":
"order-first w-full justify-start px-3 pt-2.5 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-2.5",
"block-end":
"order-last w-full justify-start px-3 pb-2.5 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-2.5",
},
},
defaultVariants: {
align: "inline-start",
},
},
);
function InputGroupAddon({
className,
align = "inline-start",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
return (
// biome-ignore lint/a11y/useSemanticElements: stock markup; div keeps layout predictable
// biome-ignore lint/a11y/useKeyWithClickEvents: focus forwarding is a pointer convenience; the control itself stays keyboard reachable
<div
role="group"
data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest("button")) {
return;
}
e.currentTarget.parentElement?.querySelector("input")?.focus();
}}
{...props}
/>
);
}
const inputGroupButtonVariants = cva(
"flex items-center gap-2 text-[13px] shadow-none",
{
variants: {
size: {
xs: "h-6 gap-1 rounded-md px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
sm: "",
"icon-xs": "size-6 rounded-md p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
},
},
defaultVariants: {
size: "xs",
},
},
);
function InputGroupButton({
className,
type = "button",
variant = "ghost",
size = "xs",
...props
}: Omit<React.ComponentProps<typeof Button>, "size" | "type"> &
VariantProps<typeof inputGroupButtonVariants> & {
type?: "button" | "submit" | "reset";
}) {
return (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
);
}
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="input-group-text"
className={cn(
"flex items-center gap-2 self-stretch bg-muted px-2.5 text-[13px] text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function InputGroupInput({
className,
...props
}: React.ComponentProps<"input">) {
return (
<Input
data-slot="input-group-control"
elevation="flat"
className={cn(
"h-auto flex-1 rounded-none border-0 bg-transparent shadow-none! focus-visible:shadow-none! disabled:bg-transparent aria-invalid:shadow-none! data-[invalid]:shadow-none!",
className,
)}
{...props}
/>
);
}
function InputGroupTextarea({
className,
...props
}: React.ComponentProps<"textarea">) {
return (
<Textarea
data-slot="input-group-control"
elevation="flat"
className={cn(
"flex-1 resize-none rounded-none border-0 bg-transparent py-2.5 shadow-none! focus-visible:shadow-none! disabled:bg-transparent aria-invalid:shadow-none!",
className,
)}
{...props}
/>
);
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
};
registry/ui/input-group/InputGroup.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
import { type Elevation, useElevation } from '@/registry/edmi/ui/elevation'
const props = withDefaults(defineProps<{
class?: HTMLAttributes['class']
/** ✦ depth: sunken -1, flat 0, raised +1, floating +2 */
elevation?: Elevation
}>(), { elevation: undefined })
// ✦ depth (v4): the group is the field; focus-within swaps the edge for the ring
const groupElevation = {
sunken: 'border-sk-bd bg-sk-bg shadow-sunken has-[[data-slot=input-group-control]:focus-visible]:bg-card [&_[data-slot=input-group-addon]]:bg-transparent! [&_[data-slot=input-group-text]]:bg-transparent!',
flat: '',
raised: 'border-transparent bg-[image:linear-gradient(var(--bv-face-b),var(--bv-face-b))] shadow-raised has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:shadow-ring [&_[data-slot=input-group-addon]]:bg-transparent! [&_[data-slot=input-group-text]]:bg-transparent!',
floating: 'border-transparent bg-[image:linear-gradient(var(--bv-face-b),var(--bv-face-b))] shadow-floating has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:shadow-ring [&_[data-slot=input-group-addon]]:bg-transparent! [&_[data-slot=input-group-text]]:bg-transparent!',
}
const level = useElevation(() => props.elevation, 'field')
</script>
<template>
<div
data-slot="input-group"
role="group"
:class="cn(
'group/input-group relative flex h-9 w-full min-w-0 items-stretch overflow-hidden rounded-md border border-input bg-card outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:shadow-none has-disabled:bg-muted has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:shadow-ring has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:shadow-ring-error has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3',
groupElevation[level],
props.class,
)"
>
<slot />
</div>
</template>
registry/ui/input-group/InputGroupAddon.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import type { InputGroupVariants } from '.'
import { cn } from '@/registry/edmi/lib/utils'
import { inputGroupAddonVariants } from '.'
const props = withDefaults(defineProps<{
align?: InputGroupVariants['align']
class?: HTMLAttributes['class']
}>(), {
align: 'inline-start',
})
function handleInputGroupAddonClick(e: MouseEvent) {
const currentTarget = e.currentTarget as HTMLElement | null
const target = e.target as HTMLElement | null
if (target && target.closest('button')) {
return
}
if (currentTarget && currentTarget?.parentElement) {
currentTarget.parentElement?.querySelector('input')?.focus()
}
}
</script>
<template>
<div
role="group"
data-slot="input-group-addon"
:data-align="props.align"
:class="cn(inputGroupAddonVariants({ align: props.align }), props.class)"
@click="handleInputGroupAddonClick"
>
<slot />
</div>
</template>
registry/ui/input-group/InputGroupButton.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import type { InputGroupButtonVariants } from '.'
import type { ButtonVariants } from '@/registry/edmi/ui/button'
import { cn } from '@/registry/edmi/lib/utils'
import { Button } from '@/registry/edmi/ui/button'
import { inputGroupButtonVariants } from '.'
interface InputGroupButtonProps {
variant?: ButtonVariants['variant']
size?: InputGroupButtonVariants['size']
class?: HTMLAttributes['class']
}
const props = withDefaults(defineProps<InputGroupButtonProps>(), {
size: 'xs',
variant: 'ghost',
})
</script>
<template>
<Button
type="button"
:data-size="props.size"
:variant="props.variant"
:class="cn(inputGroupButtonVariants({ size: props.size }), props.class)"
>
<slot />
</Button>
</template>
registry/ui/input-group/InputGroupInput.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
import { Input } from '@/registry/edmi/ui/input'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<Input
data-slot="input-group-control"
elevation="flat"
:class="cn(
'h-auto flex-1 rounded-none border-0 bg-transparent shadow-none! focus-visible:shadow-none! disabled:bg-transparent aria-invalid:shadow-none! data-[invalid]:shadow-none!',
props.class,
)"
/>
</template>
registry/ui/input-group/InputGroupText.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<span
data-slot="input-group-text"
:class="cn(
`flex items-center gap-2 self-stretch bg-muted px-2.5 text-[13px] text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4`,
props.class,
)"
>
<slot />
</span>
</template>
registry/ui/input-group/InputGroupTextarea.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
import { Textarea } from '@/registry/edmi/ui/textarea'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<Textarea
data-slot="input-group-control"
elevation="flat"
:class="cn(
'flex-1 resize-none rounded-none border-0 bg-transparent py-2.5 shadow-none! focus-visible:shadow-none! disabled:bg-transparent aria-invalid:shadow-none!',
props.class,
)"
/>
</template>
registry/ui/input-group/index.ts
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
export { default as InputGroup } from "./InputGroup.vue";
export { default as InputGroupAddon } from "./InputGroupAddon.vue";
export { default as InputGroupButton } from "./InputGroupButton.vue";
export { default as InputGroupInput } from "./InputGroupInput.vue";
export { default as InputGroupText } from "./InputGroupText.vue";
export { default as InputGroupTextarea } from "./InputGroupTextarea.vue";
export const inputGroupAddonVariants = cva(
"flex h-auto cursor-text items-center justify-center gap-2 text-[13px] font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 has-[>[data-slot=input-group-text]]:bg-muted has-[>[data-slot=input-group-text]]:px-0 [&>kbd]:rounded-[5px] [&>svg:not([class*='size-'])]:size-4",
{
variants: {
align: {
"inline-start":
"order-first pl-3 has-[>[data-slot=input-group-text]]:border-r has-[>[data-slot=input-group-text]]:border-input has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
"inline-end":
"order-last pr-2.5 has-[>[data-slot=input-group-text]]:border-l has-[>[data-slot=input-group-text]]:border-input has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
"block-start":
"order-first w-full justify-start px-3 pt-2.5 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-2.5",
"block-end":
"order-last w-full justify-start px-3 pb-2.5 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-2.5",
},
},
defaultVariants: {
align: "inline-start",
},
},
);
export type InputGroupVariants = VariantProps<typeof inputGroupAddonVariants>;
export const inputGroupButtonVariants = cva(
"flex items-center gap-2 text-[13px] shadow-none",
{
variants: {
size: {
xs: "h-6 gap-1 rounded-md px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
sm: "",
"icon-xs": "size-6 rounded-md p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
},
},
defaultVariants: {
size: "xs",
},
},
);
export type InputGroupButtonVariants = VariantProps<
typeof inputGroupButtonVariants
>;
src/lib/registry/ui/input-group/input-group-addon.svelte
<script lang="ts" module>
import { tv, type VariantProps } from "tailwind-variants";
export const inputGroupAddonVariants = tv({
base: "flex h-auto cursor-text items-center justify-center gap-2 text-[13px] font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 has-[>[data-slot=input-group-text]]:bg-muted has-[>[data-slot=input-group-text]]:px-0 [&>kbd]:rounded-[5px] [&>svg:not([class*='size-'])]:size-4",
variants: {
align: {
"inline-start":
"order-first pl-3 has-[>[data-slot=input-group-text]]:border-r has-[>[data-slot=input-group-text]]:border-input has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
"inline-end":
"order-last pr-2.5 has-[>[data-slot=input-group-text]]:border-l has-[>[data-slot=input-group-text]]:border-input has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
"block-start":
"order-first w-full justify-start px-3 pt-2.5 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-2.5",
"block-end":
"order-last w-full justify-start px-3 pb-2.5 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-2.5",
},
},
defaultVariants: {
align: "inline-start",
},
});
export type InputGroupAddonAlign = VariantProps<typeof inputGroupAddonVariants>["align"];
</script>
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
align = "inline-start",
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
align?: InputGroupAddonAlign;
} = $props();
</script>
<div
bind:this={ref}
role="group"
data-slot="input-group-addon"
data-align={align}
class={cn(inputGroupAddonVariants({ align }), className)}
onclick={(e) => {
if ((e.target as HTMLElement).closest("button")) {
return;
}
e.currentTarget.parentElement?.querySelector("input")?.focus();
}}
{...restProps}
>
{@render children?.()}
</div>
src/lib/registry/ui/input-group/input-group-button.svelte
<script lang="ts" module>
import { tv, type VariantProps } from "tailwind-variants";
const inputGroupButtonVariants = tv({
base: "flex items-center gap-2 text-[13px] shadow-none",
variants: {
size: {
xs: "h-6 gap-1 rounded-md px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
sm: "",
"icon-xs": "size-6 rounded-md p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
},
},
defaultVariants: {
size: "xs",
},
});
export type InputGroupButtonSize = VariantProps<typeof inputGroupButtonVariants>["size"];
</script>
<script lang="ts">
import { Button } from "$lib/registry/ui/button/index.js";
import { cn } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
class: className,
children,
type = "button",
variant = "ghost",
size = "xs",
...restProps
}: Omit<ComponentProps<typeof Button>, "href" | "size"> & {
size?: InputGroupButtonSize;
} = $props();
</script>
<Button
bind:ref
{type}
data-size={size}
{variant}
class={cn(inputGroupButtonVariants({ size }), className)}
{...restProps}
>
{@render children?.()}
</Button>
src/lib/registry/ui/input-group/input-group-input.svelte
<script lang="ts">
import { Input } from "$lib/registry/ui/input/index.js";
import { cn } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
value = $bindable(),
class: className,
...props
}: ComponentProps<typeof Input> = $props();
</script>
<Input
bind:ref
data-slot="input-group-control"
class={cn("h-auto flex-1 rounded-none border-0 bg-transparent shadow-none! focus-visible:shadow-none! disabled:bg-transparent aria-invalid:shadow-none! data-[invalid]:shadow-none!", className)}
bind:value
{...props}
elevation="flat"
/>
src/lib/registry/ui/input-group/input-group-text.svelte
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
data-slot="input-group-text"
class={cn(
"flex items-center gap-2 self-stretch bg-muted px-2.5 text-[13px] text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...restProps}
>
{@render children?.()}
</span>
src/lib/registry/ui/input-group/input-group-textarea.svelte
<script lang="ts">
import { Textarea } from "$lib/registry/ui/textarea/index.js";
import { cn } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
value = $bindable(),
class: className,
...props
}: ComponentProps<typeof Textarea> = $props();
</script>
<Textarea
bind:ref
data-slot="input-group-control"
class={cn("flex-1 resize-none rounded-none border-0 bg-transparent py-2.5 shadow-none! focus-visible:shadow-none! disabled:bg-transparent aria-invalid:shadow-none!", className)}
bind:value
{...props}
elevation="flat"
/>
src/lib/registry/ui/input-group/input-group.svelte
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import { type Elevation, useElevation } from "$lib/registry/ui/elevation/index.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
elevation = "auto",
...props
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
/** ✦ depth: sunken -1, flat 0, raised +1, floating +2. */
elevation?: Elevation;
} = $props();
// ✦ depth (v4): the group is the field; focus-within swaps the edge for the ring
const groupElevation = {
sunken: "border-sk-bd bg-sk-bg shadow-sunken has-[[data-slot=input-group-control]:focus-visible]:bg-card [&_[data-slot=input-group-addon]]:bg-transparent! [&_[data-slot=input-group-text]]:bg-transparent!",
flat: "",
raised: "border-transparent bg-[image:linear-gradient(var(--bv-face-b),var(--bv-face-b))] shadow-raised has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:shadow-ring [&_[data-slot=input-group-addon]]:bg-transparent! [&_[data-slot=input-group-text]]:bg-transparent!",
floating: "border-transparent bg-[image:linear-gradient(var(--bv-face-b),var(--bv-face-b))] shadow-floating has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:shadow-ring [&_[data-slot=input-group-addon]]:bg-transparent! [&_[data-slot=input-group-text]]:bg-transparent!",
};
const level = useElevation(() => elevation, "field");
</script>
<div
bind:this={ref}
data-slot="input-group"
role="group"
class={cn(
"group/input-group relative flex h-9 w-full min-w-0 items-stretch overflow-hidden rounded-md border border-input bg-card outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:shadow-none has-disabled:bg-muted has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:shadow-ring has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:shadow-ring-error has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3",
groupElevation[level.current],
className
)}
{...props}
>
{@render children?.()}
</div>
src/lib/registry/ui/input-group/index.ts
import Root from "./input-group.svelte";
import Addon from "./input-group-addon.svelte";
import Button from "./input-group-button.svelte";
import Input from "./input-group-input.svelte";
import Text from "./input-group-text.svelte";
import Textarea from "./input-group-textarea.svelte";
export {
Addon,
Addon as InputGroupAddon,
Button,
Button as InputGroupButton,
Input,
Input as InputGroupInput,
Root,
//
Root as InputGroup,
Text,
Text as InputGroupText,
Textarea,
Textarea as InputGroupTextarea,
};

See the Elevation guide for the levels, roles and layered mode.

The group is the field: elevation applies to the whole group (inner input and addons stay flat). sunken is the layered default.

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Flat (0)

USDC

Raised (+1)

USDC

Floating (+2)

USDC

Sunken (-1)

USDC

Flat (0)

USDC

Raised (+1)

USDC

Floating (+2)

USDC

Sunken (-1)

USDC

Flat (0)

USDC

Raised (+1)

USDC

Floating (+2)

USDC

Sunken (-1)

USDC
Prop Type Default
InputGroupAddon align inline-start | inline-end | block-start | block-end inline-start
InputGroupButton size xs | sm | icon-xs | icon-sm xs
InputGroupButton variant Button variants ghost
elevation "auto" | "sunken" | "flat" | "raised" | "floating" "auto"
  • InputGroupText addons render as a muted segment with a divider.