Dice UI
Components

Input Group

A group of connected input fields that appear as a single visual unit.

"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"
pnpm dlx shadcn@latest add "https://diceui.com/r/input-group"
yarn dlx shadcn@latest add "https://diceui.com/r/input-group"
bun x shadcn@latest add "https://diceui.com/r/input-group"

Manual

Install the following dependencies:

npm install class-variance-authority
pnpm add class-variance-authority
yarn add class-variance-authority
bun add 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 arrow keys (up/down) to navigate between fields in 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.

PropTypeDefault
required?
boolean
false
invalid?
boolean
false
disabled?
boolean
false
asChild?
boolean
false
size?
"sm" | "default" | "lg"
"default"
orientation?
"horizontal" | "vertical"
"horizontal"
dir?
Direction
"ltr"
id?
string
-

InputGroupItem

Individual input items within the input group.

PropTypeDefault
required?
boolean
-
disabled?
boolean
-
asChild?
boolean
false
position?
"first" | "middle" | "last" | "isolated"
"isolated"

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 border
    • middle: Removes all border radius, removes left border to prevent doubling
    • last: Removes left border radius, removes left border to prevent doubling
    • isolated: 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

KeyDescription
TabMoves focus to the next input in the group.
ShiftTabMoves focus to the previous input in the group.