"use client";
import * as React from "react";
import { InputGroup, InputGroupItem } from "@/components/ui/input-group";
export function InputGroupDemo() {
const [values, setValues] = React.useState({
first: "",
second: "",
third: "",
});
const onValueChange = React.useCallback(
(field: keyof typeof values) =>
(event: React.ChangeEvent<HTMLInputElement>) => {
setValues((prev) => ({
...prev,
[field]: event.target.value,
}));
},
[],
);
return (
<div className="flex flex-col gap-2">
<label className="font-medium text-sm leading-none">
Enter your details
</label>
<InputGroup className="w-full max-w-sm">
<InputGroupItem
position="first"
placeholder="First"
value={values.first}
onChange={onValueChange("first")}
aria-label="First name"
/>
<InputGroupItem
position="middle"
placeholder="Second"
value={values.second}
onChange={onValueChange("second")}
aria-label="Middle name"
/>
<InputGroupItem
position="last"
placeholder="Third"
value={values.third}
onChange={onValueChange("third")}
aria-label="Last name"
/>
</InputGroup>
</div>
);
}
Installation
CLI
npx shadcn@latest add "https://diceui.com/r/input-group"
Manual
Install the following dependencies:
npm install class-variance-authority
Copy and paste the following code into your project.
"use client";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
const ROOT_NAME = "InputGroup";
const ITEM_NAME = "InputGroupItem";
type Direction = "ltr" | "rtl";
const DirectionContext = React.createContext<Direction | undefined>(undefined);
function useDirection(dirProp?: Direction): Direction {
const contextDir = React.useContext(DirectionContext);
return dirProp ?? contextDir ?? "ltr";
}
interface InputGroupContextValue {
dir?: Direction;
orientation?: "horizontal" | "vertical";
size?: "sm" | "default" | "lg";
disabled?: boolean;
invalid?: boolean;
required?: boolean;
}
const InputGroupContext = React.createContext<InputGroupContextValue | null>(
null,
);
InputGroupContext.displayName = ROOT_NAME;
function useInputGroupContext(consumerName: string) {
const context = React.useContext(InputGroupContext);
if (!context) {
throw new Error(`\`${consumerName}\` must be used within \`${ROOT_NAME}\``);
}
return context;
}
const inputGroupItemVariants = cva("", {
variants: {
position: {
first: "rounded-e-none",
middle: "-ms-px rounded-none border-l-0",
last: "-ms-px rounded-s-none border-l-0",
isolated: "",
},
orientation: {
horizontal: "",
vertical: "",
},
},
compoundVariants: [
{
position: "first",
orientation: "vertical",
class: "ms-0 rounded-e-md rounded-b-none border-l-1",
},
{
position: "middle",
orientation: "vertical",
class: "-mt-px ms-0 rounded-none border-t-0 border-l-1",
},
{
position: "last",
orientation: "vertical",
class: "-mt-px ms-0 rounded-s-md rounded-t-none border-t-0 border-l-1",
},
],
defaultVariants: {
position: "isolated",
orientation: "horizontal",
},
});
interface InputGroupRootProps extends React.ComponentProps<"div"> {
id?: string;
dir?: Direction;
orientation?: "horizontal" | "vertical";
size?: "sm" | "default" | "lg";
asChild?: boolean;
disabled?: boolean;
invalid?: boolean;
required?: boolean;
}
function InputGroupRoot(props: InputGroupRootProps) {
const {
dir: dirProp,
orientation = "horizontal",
size = "default",
className,
asChild,
disabled,
invalid,
required,
...rootProps
} = props;
const id = React.useId();
const dir = useDirection(dirProp);
const contextValue = React.useMemo<InputGroupContextValue>(
() => ({
dir,
orientation,
size,
disabled,
invalid,
required,
}),
[dir, orientation, size, disabled, invalid, required],
);
const RootPrimitive = asChild ? Slot : "div";
return (
<InputGroupContext.Provider value={contextValue}>
<RootPrimitive
role="group"
aria-orientation={orientation}
data-slot="input-group"
data-orientation={orientation}
data-disabled={disabled ? "" : undefined}
data-invalid={invalid ? "" : undefined}
data-required={required ? "" : undefined}
{...rootProps}
id={id}
dir={dir}
className={cn(
"flex",
orientation === "horizontal" ? "flex-row" : "flex-col",
className,
)}
/>
</InputGroupContext.Provider>
);
}
InputGroupRoot.displayName = ROOT_NAME;
interface InputGroupItemProps
extends React.ComponentProps<"input">,
VariantProps<typeof inputGroupItemVariants> {
asChild?: boolean;
}
function InputGroupItem(props: InputGroupItemProps) {
const { asChild, className, position, disabled, required, ...inputProps } =
props;
const context = useInputGroupContext(ITEM_NAME);
const isDisabled = disabled ?? context.disabled;
const isRequired = required ?? context.required;
const InputPrimitive = asChild ? Slot : Input;
return (
<InputPrimitive
data-slot="input-group-item"
data-position={position}
data-orientation={context.orientation}
data-disabled={isDisabled ? "" : undefined}
data-invalid={context.invalid ? "" : undefined}
data-required={isRequired ? "" : undefined}
aria-invalid={context.invalid}
aria-required={isRequired}
disabled={isDisabled}
required={isRequired}
{...inputProps}
className={cn(
inputGroupItemVariants({
position,
orientation: context.orientation,
}),
className,
)}
/>
);
}
InputGroupItem.displayName = ITEM_NAME;
export {
InputGroupRoot as InputGroup,
InputGroupItem,
//
InputGroupRoot as Root,
InputGroupItem as Item,
};
Layout
Import the component and use it with different position props.
import { InputGroup, InputGroupItem } from "@/components/ui/input-group";
<InputGroup>
<InputGroupItem position="first" placeholder="First" />
<InputGroupItem position="middle" placeholder="Middle" />
<InputGroupItem position="last" placeholder="Last" />
</InputGroup>
Examples
Form Input
Use grouped inputs for structured form data like phone numbers or addresses.
"use client";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { InputGroup, InputGroupItem } from "@/components/ui/input-group";
export function InputGroupFormDemo() {
const [phoneNumber, setPhoneNumber] = React.useState({
countryCode: "+1",
areaCode: "",
number: "",
});
const onSubmit = React.useCallback(
(event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log(
"Phone number:",
`${phoneNumber.countryCode} ${phoneNumber.areaCode}-${phoneNumber.number}`,
);
},
[phoneNumber],
);
return (
<form onSubmit={onSubmit} className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<label className="font-medium text-sm leading-none">Phone Number</label>
<InputGroup className="w-full max-w-sm" aria-label="Phone number input">
<InputGroupItem
position="first"
placeholder="+1"
value={phoneNumber.countryCode}
onChange={(event) =>
setPhoneNumber((prev) => ({
...prev,
countryCode: event.target.value,
}))
}
className="w-16"
aria-label="Country code"
/>
<InputGroupItem
position="middle"
placeholder="555"
value={phoneNumber.areaCode}
onChange={(event) =>
setPhoneNumber((prev) => ({
...prev,
areaCode: event.target.value,
}))
}
className="w-20"
maxLength={3}
inputMode="numeric"
pattern="[0-9]*"
aria-label="Area code"
/>
<InputGroupItem
position="last"
placeholder="1234567"
value={phoneNumber.number}
onChange={(event) =>
setPhoneNumber((prev) => ({
...prev,
number: event.target.value,
}))
}
className="flex-1"
maxLength={7}
inputMode="numeric"
pattern="[0-9]*"
aria-label="Phone number"
/>
</InputGroup>
</div>
<Button type="submit" size="sm">
Submit
</Button>
</form>
);
}
RGB Color Input
Create color input controls using grouped inputs for RGB values.
"use client";
import * as React from "react";
import * as InputGroup from "@/components/ui/input-group";
export function InputGroupRgbDemo() {
const [rgb, setRgb] = React.useState({
r: 255,
g: 128,
b: 0,
});
const onChannelChange = React.useCallback(
(channel: keyof typeof rgb) =>
(event: React.ChangeEvent<HTMLInputElement>) => {
const value = Number.parseInt(event.target.value, 10);
if (!Number.isNaN(value) && value >= 0 && value <= 255) {
setRgb((prev) => ({
...prev,
[channel]: value,
}));
}
},
[],
);
return (
<div className="flex flex-col gap-2">
<label className="font-medium text-sm leading-none">RGB Color</label>
<InputGroup.Root className="w-fit" aria-label="RGB color input">
<InputGroup.Item
position="first"
placeholder="255"
value={rgb.r}
onChange={onChannelChange("r")}
className="w-16"
inputMode="numeric"
pattern="[0-9]*"
min="0"
max="255"
aria-label="Red channel (0-255)"
/>
<InputGroup.Item
position="middle"
placeholder="128"
value={rgb.g}
onChange={onChannelChange("g")}
className="w-16"
inputMode="numeric"
pattern="[0-9]*"
min="0"
max="255"
aria-label="Green channel (0-255)"
/>
<InputGroup.Item
position="last"
placeholder="0"
value={rgb.b}
onChange={onChannelChange("b")}
className="w-16"
inputMode="numeric"
pattern="[0-9]*"
min="0"
max="255"
aria-label="Blue channel (0-255)"
/>
</InputGroup.Root>
</div>
);
}
Vertical Layout
Display input groups in a vertical orientation.
"use client";
import * as React from "react";
import { InputGroup, InputGroupItem } from "@/components/ui/input-group";
export function InputGroupVerticalDemo() {
const [address, setAddress] = React.useState({
street: "",
city: "",
zipCode: "",
});
const onFieldChange = React.useCallback(
(field: keyof typeof address) =>
(event: React.ChangeEvent<HTMLInputElement>) => {
setAddress((prev) => ({
...prev,
[field]: event.target.value,
}));
},
[],
);
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<label className="font-medium text-sm leading-none">
Mailing Address
</label>
<InputGroup
aria-label="Mailing address input"
className="w-full max-w-sm"
orientation="vertical"
>
<InputGroupItem
aria-label="Street address"
position="first"
placeholder="Street Address"
value={address.street}
onChange={onFieldChange("street")}
/>
<InputGroupItem
aria-label="City"
position="middle"
placeholder="City"
value={address.city}
onChange={onFieldChange("city")}
/>
<InputGroupItem
aria-label="ZIP code"
position="last"
placeholder="ZIP Code"
value={address.zipCode}
onChange={onFieldChange("zipCode")}
/>
</InputGroup>
</div>
<p className="text-muted-foreground text-sm">
Use arrow keys (up/down) to navigate between fields in vertical
orientation.
</p>
</div>
);
}
API Reference
InputGroup
The main input group container component.
Prop
Type
InputGroupItem
Individual input items within the input group.
Prop
Type
The component supports all standard HTML input attributes and properties from the base Input component, plus the following:
- position: Controls the visual positioning and borders of the input within a group
first
: Removes right border radius, maintains left bordermiddle
: Removes all border radius, removes left border to prevent doublinglast
: Removes left border radius, removes left border to prevent doublingisolated
: Normal input styling (default)
Styling
The component automatically handles:
- Border radius removal for seamless connection
- Border overlap prevention with negative margins
- Focus ring z-index management
- Webkit number input spinner removal
- Firefox number input appearance normalization
Accessibility
The InputGroup component follows standard web accessibility practices. Users navigate between inputs using Tab and Shift+Tab keys, which is the expected behavior for form controls.
Keyboard Interactions
Key | Description |
---|---|
Tab | Moves focus to the next input in the group. |
ShiftTab | Moves focus to the previous input in the group. |