Input Group
Icons, text, buttons or keys around an input sharing one focus ring.
import { Button } from "@/components/ui/button";import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText, InputGroupTextarea,} from "@/components/ui/input-group";import { IconPlaceholder } from "@/edmi/icon-placeholder";
export default function Demo() { return ( <div className="grid w-full max-w-sm gap-3"> <InputGroup> <InputGroupAddon> <IconPlaceholder lucide="SearchIcon" tabler="IconSearch" hugeicons="SearchIcon" phosphor="MagnifyingGlassIcon" remixicon="RiSearchLine" /> </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> <InputGroup> <InputGroupInput defaultValue="7xKXtg2CW87d97Qp3h" readOnly /> <InputGroupAddon align="inline-end"> <InputGroupButton size="icon-xs" aria-label="Copy"> <IconPlaceholder lucide="CopyIcon" tabler="IconCopy" hugeicons="CopyIcon" phosphor="CopyIcon" remixicon="RiFileCopyLine" /> </InputGroupButton> </InputGroupAddon> </InputGroup> <InputGroup> <InputGroupTextarea placeholder="Describe your thesis..." /> <InputGroupAddon align="block-end"> <InputGroupText>52%</InputGroupText> <Button size="icon-xs" className="ml-auto" aria-label="Send"> <IconPlaceholder lucide="ArrowUpIcon" tabler="IconArrowUp" hugeicons="ArrowUpIcon" phosphor="ArrowUpIcon" remixicon="RiArrowUpLine" /> </Button> </InputGroupAddon> </InputGroup> </div> );}<script setup lang="ts">import { ArrowUpIcon, CopyIcon, SearchIcon } from "@lucide/vue";import { Button } from "@/components/ui/button";import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText, InputGroupTextarea,} 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> <InputGroup> <InputGroupInput default-value="7xKXtg2CW87d97Qp3h" readonly /> <InputGroupAddon align="inline-end"> <InputGroupButton size="icon-xs" aria-label="Copy"> <CopyIcon /> </InputGroupButton> </InputGroupAddon> </InputGroup> <InputGroup> <InputGroupTextarea placeholder="Describe your thesis..." /> <InputGroupAddon align="block-end"> <InputGroupText>52%</InputGroupText> <Button size="icon-xs" class="ml-auto" aria-label="Send"> <ArrowUpIcon /> </Button> </InputGroupAddon> </InputGroup> </div></template><script lang="ts"> import ArrowUpIcon from "phosphor-svelte/lib/ArrowUp"; import CopyIcon from "phosphor-svelte/lib/Copy"; import MagnifyingGlassIcon from "phosphor-svelte/lib/MagnifyingGlass"; import { Button } from "$lib/components/ui/button"; import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText, InputGroupTextarea, } from "$lib/components/ui/input-group";
let query = $state("");</script>
<div class="grid w-full max-w-sm gap-3"> <InputGroup> <InputGroupAddon> <MagnifyingGlassIcon /> </InputGroupAddon> <InputGroupInput bind:value={query} 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> <InputGroup> <InputGroupInput value="7xKXtg2CW87d97Qp3h" readonly /> <InputGroupAddon align="inline-end"> <InputGroupButton size="icon-xs" aria-label="Copy"> <CopyIcon /> </InputGroupButton> </InputGroupAddon> </InputGroup> <InputGroup> <InputGroupTextarea placeholder="Describe your thesis..." /> <InputGroupAddon align="block-end"> <InputGroupText>52%</InputGroupText> <Button size="icon-xs" class="ml-auto" aria-label="Send"> <ArrowUpIcon /> </Button> </InputGroupAddon> </InputGroup></div>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/input-grouppnpm dlx shadcn@latest add @edmi-ui/input-groupyarn dlx shadcn@latest add @edmi-ui/input-groupbunx shadcn@latest add @edmi-ui/input-groupPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install class-variance-authorityTerminal window pnpm add class-variance-authorityTerminal window yarn add class-variance-authorityTerminal window bun add class-variance-authorityAdd the Edmi components this one builds on:
button,input,textarea,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/input-grouppnpm dlx shadcn-vue@latest add @edmi-ui/input-groupyarn dlx shadcn-vue@latest add @edmi-ui/input-groupbunx shadcn-vue@latest add @edmi-ui/input-groupPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install class-variance-authorityTerminal window pnpm add class-variance-authorityTerminal window yarn add class-variance-authorityTerminal window bun add class-variance-authorityAdd the Edmi components this one builds on:
button,input,textarea,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/input-group.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/input-group.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/input-group.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/input-group.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
button,input,textarea,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
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>Component source
Section titled “Component source”"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 ringconst 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,};<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 ringconst 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><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><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><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><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><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>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>;<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><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><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"/><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><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"/><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>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,};Elevation ✦
Section titled “Elevation ✦”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.
Flat (0)
Raised (+1)
Floating (+2)
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
Sunken (-1)
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText,} from "@/components/ui/input-group";import { IconPlaceholder } from "@/edmi/icon-placeholder";
const levels = [ { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }, { value: "sunken", label: "Sunken (-1)" },] as const;
export default function Demo() { return ( <div className="flex w-full max-w-2xl flex-col gap-6"> {levels.map(({ value, label }) => ( <div key={value} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground">{label}</p> <div className="grid gap-3 sm:grid-cols-2"> <InputGroup elevation={value}> <InputGroupAddon> <IconPlaceholder lucide="SearchIcon" tabler="IconSearch" hugeicons="SearchIcon" phosphor="MagnifyingGlassIcon" remixicon="RiSearchLine" /> </InputGroupAddon> <InputGroupInput placeholder="Search indexes" /> </InputGroup> <InputGroup elevation={value}> <InputGroupInput defaultValue="1,000.00" className="font-mono tabular-nums" /> <InputGroupAddon align="inline-end"> <InputGroupText>USDC</InputGroupText> </InputGroupAddon> </InputGroup> <InputGroup elevation={value}> <InputGroupInput defaultValue="7xKXtg2C…Qp3h" readOnly className="font-mono text-[13px]" /> <InputGroupAddon align="inline-end"> <InputGroupButton size="icon-xs" aria-label="Copy"> <IconPlaceholder lucide="CopyIcon" tabler="IconCopy" hugeicons="CopyIcon" phosphor="CopyIcon" remixicon="RiFileCopyLine" /> </InputGroupButton> </InputGroupAddon> </InputGroup> </div> </div> ))} </div> );}<script setup lang="ts">import { CopyIcon, SearchIcon } from "@lucide/vue";import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText,} from "@/components/ui/input-group";
const levels = [ { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }, { value: "sunken", label: "Sunken (-1)" },] as const;</script>
<template> <div class="flex w-full max-w-2xl flex-col gap-6"> <div v-for="level in levels" :key="level.value" class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{{ level.label }}</p> <div class="grid gap-3 sm:grid-cols-2"> <InputGroup :elevation="level.value"> <InputGroupAddon> <SearchIcon /> </InputGroupAddon> <InputGroupInput placeholder="Search indexes" /> </InputGroup> <InputGroup :elevation="level.value"> <InputGroupInput default-value="1,000.00" class="font-mono tabular-nums" /> <InputGroupAddon align="inline-end"> <InputGroupText>USDC</InputGroupText> </InputGroupAddon> </InputGroup> <InputGroup :elevation="level.value"> <InputGroupInput default-value="7xKXtg2C…Qp3h" readonly class="font-mono text-[13px]" /> <InputGroupAddon align="inline-end"> <InputGroupButton size="icon-xs" aria-label="Copy"> <CopyIcon /> </InputGroupButton> </InputGroupAddon> </InputGroup> </div> </div> </div></template><script lang="ts"> import CopyIcon from "phosphor-svelte/lib/Copy"; import MagnifyingGlassIcon from "phosphor-svelte/lib/MagnifyingGlass"; import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText, } from "$lib/components/ui/input-group";
const levels = [ { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }, { value: "sunken", label: "Sunken (-1)" }, ] as const;</script>
<div class="flex w-full max-w-2xl flex-col gap-6"> {#each levels as level (level.value)} <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{level.label}</p> <div class="grid gap-3 sm:grid-cols-2"> <InputGroup elevation={level.value}> <InputGroupAddon> <MagnifyingGlassIcon /> </InputGroupAddon> <InputGroupInput placeholder="Search indexes" /> </InputGroup> <InputGroup elevation={level.value}> <InputGroupInput value="1,000.00" class="font-mono tabular-nums" /> <InputGroupAddon align="inline-end"> <InputGroupText>USDC</InputGroupText> </InputGroupAddon> </InputGroup> <InputGroup elevation={level.value}> <InputGroupInput value="7xKXtg2C…Qp3h" readonly class="font-mono text-[13px]" /> <InputGroupAddon align="inline-end"> <InputGroupButton size="icon-xs" aria-label="Copy"> <CopyIcon /> </InputGroupButton> </InputGroupAddon> </InputGroup> </div> </div> {/each}</div>| 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" |
✦ Edmi additions
Section titled “✦ Edmi additions”InputGroupTextaddons render as a muted segment with a divider.