- Accordion
- AppShell
- Autocomplete
- Badge
- Blobatar
- BoringAvatar
- Button
- Calendar
- Checkbox
- DataGrid
- Date Picker
- Date Range Picker
- DateTime
- Dialog
- Drawer
- Dropdown
- Empty
- Error State
- Frame
- Input
- Input Amount
- Input Group
- Input OTP
- Input Password
- Input Phone
- Input Search
- Input Secret
- Loading
- PageContainer
- PageHeader
- Popover
- Select
- StatsCard
- Switch
- Tabs
- TextArea
- Toast
- Tooltip
Playground
"use client";
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "@/components/ui/input-group";
export function InputGroupDemo() {
return (
<InputGroup>
<InputGroupAddon align={"inline-start"}>
<InputGroupText>https://</InputGroupText>
</InputGroupAddon>
<InputGroupInput label={"Website"} labelStyle={"floating"} placeholder="example.com" />
</InputGroup>
);
}Installation
Initialize shadcn in your React project, then install this component from the Vandor UI registry:
$ pnpm dlx shadcn@latest add https://vandor-ui.vercel.app/r/input-group.json
The default installation target is components/ui/input-group.tsx. Review the CLI's overwrite prompt before replacing local customizations.
Dependencies
Installed automatically by the registry CLI. For manual setup:
- cn
$ pnpm add cnUsage
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
} from "@/components/ui/input-group";
<InputGroup>
<InputGroupAddon>
<InputGroupText>https://</InputGroupText>
</InputGroupAddon>
<InputGroupInput label="Website" placeholder="example.com" />
</InputGroup>;InputGroupAddon supports inline-start/end and block-start/end placement. Use InputGroupButton for an interactive action.
InputGroupInput accepts the same label, icon, and native input props as Input. Floating labels rise to a border notch without a background patch; an inline group remains 40px tall. Use labelStyle="static" for a conventional label. Inline addons stay beside the flexible control; block addons occupy their own row.
Examples
The addon is decorative; the editable field keeps its own accessible name.
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
} from "@/components/ui/input-group";
export const InputGroupDemo = () => (
<InputGroup className="max-w-sm">
<InputGroupAddon>
<InputGroupText>https://</InputGroupText>
</InputGroupAddon>
<InputGroupInput label="Website domain" placeholder="example.com" />
</InputGroup>
);Optional Storybook Stories
Install Input Group first, then run npx shadcn@latest add https://vandor-ui.vercel.app/r/input-group-stories.json. Stories cover all four addon positions, a local search action, textarea composition, and disabled/read-only/invalid states. Controls configure the child input and addon, not the group div. The action example makes no network request.
Stories install beside Input Group under your configured UI alias and use relative imports. Use your existing matching @storybook/react packages (verified with 10.6.1), discover **/*.stories.tsx, and load your global Tailwind/theme stylesheet. No addons, extra packages, core overwrites, or Storybook config edits are required. Default-value Controls remount uncontrolled examples; WithAction owns local state. Storybook UI is not verified because no server exists here.
Behavior & Accessibility
The editable child remains a native input or textarea. Give it its own accessible name; decorative addons do not add tab stops. Group focus and invalid styles follow the contained control.
Props
| Prop | Type / Description | Default |
|---|---|---|
| label | stringInputGroupInput: visible associated label. | undefined |
| labelStyle | "floating" | "static"InputGroupInput: label presentation. | "floating" |
| align | "inline-start" | "inline-end" | "block-start" | "block-end"InputGroupAddon: addon position. | "inline-start" |
| children | ReactNodeInputGroup and addon components: composed controls/content. | required |
| className | stringGroup/addon/control: merged class name. | undefined |
| disabled | booleanInputGroupInput: disables the native input. | false |
| type | "button" | "submit" | "reset"InputGroupButton: native button type. | "button" |
The reference lists component-specific props and selected native props, not the complete React API. Other native attributes are forwarded to the rendered element.
Source
"use client";
import { cn } from "cn";
import type { ComponentProps } from "react";
import * as React from "react";
import { fieldSurfaceClassName, Input } from "./input";
import type { InputProps } from "./input";
export const InputGroup = ({ className, ...props }: ComponentProps<"div">) => (
<div
data-slot="input-group"
className={cn(
"group/input-group flex w-full min-w-0 flex-wrap items-stretch rounded-md border border-input shadow-xs outline-none focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50 has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50 has-[[aria-invalid=true]]:border-destructive",
"has-[[data-slot=floating-input]]:not-has-[[data-align=block-start]]:border-t-transparent",
"has-[[data-slot=floating-input]]:focus-within:ring-0",
fieldSurfaceClassName,
className
)}
{...props}
/>
);
export const InputGroupInput = ({ className, ...props }: InputProps) => (
<Input
data-slot="input-group-control"
containerClassName="min-w-px flex-1 basis-0 [&>[data-slot=input-outline]]:-top-[7px] [&>[data-slot=input-outline]]:rounded-none [&>[data-slot=input-outline]]:border-x-0 [&>[data-slot=input-outline]]:border-b-0 group-not-has-[[data-align=inline-start]]/input-group:group-not-has-[[data-align=block-start]]/input-group:[&>[data-slot=input-outline]]:rounded-tl-md group-not-has-[[data-align=inline-end]]/input-group:group-not-has-[[data-align=block-start]]/input-group:[&>[data-slot=input-outline]]:rounded-tr-md group-focus-within/input-group:[&>[data-slot=input-outline]]:border-ring group-has-[[aria-invalid=true]]/input-group:[&>[data-slot=input-outline]]:border-destructive"
className={cn(
"min-w-0 flex-1 basis-0 rounded-none border-0 bg-transparent bg-none shadow-none focus-visible:ring-0 disabled:opacity-100 dark:bg-transparent",
props.label && props.labelStyle !== "static" && "h-[38px]",
className
)}
{...props}
/>
);
export const InputGroupTextarea = ({
className,
...props
}: ComponentProps<"textarea">) => (
<textarea
data-slot="input-group-control"
className={cn(
"min-h-20 min-w-px flex-1 bg-transparent px-3 py-2 text-sm outline-none",
className
)}
{...props}
/>
);
export const InputGroupAddon = ({
align = "inline-start",
className,
...props
}: ComponentProps<"div"> & {
align?: "inline-start" | "inline-end" | "block-start" | "block-end";
}) => (
<div
data-slot="input-group-addon"
data-align={align}
className={cn(
"flex shrink-0 items-center justify-center gap-1.5 px-3 text-sm text-muted-foreground [&_svg:not([class*='size-'])]:size-4",
"relative before:pointer-events-none before:absolute before:inset-x-0 before:-top-px before:hidden before:h-2 before:border-input before:content-[''] group-focus-within/input-group:before:border-ring group-has-[[aria-invalid=true]]/input-group:before:border-destructive",
(align === "inline-start" || align === "inline-end") &&
"group-has-[[data-slot=floating-input]]/input-group:before:block before:border-t",
align === "inline-start" &&
"before:-left-px before:rounded-tl-md before:border-l group-has-[[data-align=block-start]]/input-group:before:rounded-none group-has-[[data-align=block-start]]/input-group:before:border-l-0",
align === "inline-end" &&
"before:-right-px before:rounded-tr-md before:border-r group-has-[[data-align=block-start]]/input-group:before:rounded-none group-has-[[data-align=block-start]]/input-group:before:border-r-0",
align === "inline-start" && "order-first",
align === "inline-end" && "order-last",
align === "block-start" &&
"order-first w-full justify-start border-b px-3 py-2 group-has-[[data-slot=floating-input]]/input-group:border-b-transparent",
align === "block-end" &&
"order-last w-full justify-start border-t px-3 py-2",
className
)}
{...props}
/>
);
export const InputGroupText = ({
className,
...props
}: ComponentProps<"span">) => (
<span
data-slot="input-group-text"
className={cn("text-muted-foreground", className)}
{...props}
/>
);
export const InputGroupButton = ({
className,
type = "button",
...props
}: ComponentProps<"button">) => (
<button
data-slot="input-group-button"
type={type}
className={cn(
"inline-flex shrink-0 items-center justify-center gap-2 rounded-sm px-2 text-sm font-medium outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50",
className
)}
{...props}
/>
);