Dice UI
Components

Segmented Input

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

API
"use client";
 
import * as React from "react";
import {
  SegmentedInput,
  SegmentedInputItem,
} from "@/components/ui/segmented-input";
 
export function SegmentedInputDemo() {
  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>
      <SegmentedInput className="w-full max-w-sm">
        <SegmentedInputItem
          placeholder="First"
          value={values.first}
          onChange={onValueChange("first")}
          aria-label="First name"
        />
        <SegmentedInputItem
          placeholder="Second"
          value={values.second}
          onChange={onValueChange("second")}
          aria-label="Middle name"
        />
        <SegmentedInputItem
          placeholder="Third"
          value={values.third}
          onChange={onValueChange("third")}
          aria-label="Last name"
        />
      </SegmentedInput>
    </div>
  );
}

Installation

CLI

npx shadcn@latest add "https://diceui.com/r/segmented-input"

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 = "SegmentedInput";
const ITEM_NAME = "SegmentedInputItem";
 
type Direction = "ltr" | "rtl";
type Orientation = "horizontal" | "vertical";
type Size = "default" | "sm" | "lg";
type Position = "isolated" | "first" | "middle" | "last";
 
const DirectionContext = React.createContext<Direction | undefined>(undefined);
 
function useDirection(dirProp?: Direction): Direction {
  const contextDir = React.useContext(DirectionContext);
  return dirProp ?? contextDir ?? "ltr";
}
 
interface SegmentedInputContextValue {
  dir?: Direction;
  orientation?: Orientation;
  size?: Size;
  disabled?: boolean;
  invalid?: boolean;
  required?: boolean;
}
 
const SegmentedInputContext =
  React.createContext<SegmentedInputContextValue | null>(null);
 
function useSegmentedInputContext(consumerName: string) {
  const context = React.useContext(SegmentedInputContext);
  if (!context) {
    throw new Error(`\`${consumerName}\` must be used within \`${ROOT_NAME}\``);
  }
  return context;
}
 
interface SegmentedInputProps extends React.ComponentProps<"div"> {
  dir?: Direction;
  orientation?: Orientation;
  size?: Size;
  asChild?: boolean;
  disabled?: boolean;
  invalid?: boolean;
  required?: boolean;
}
 
function SegmentedInput(props: SegmentedInputProps) {
  const {
    size = "default",
    dir: dirProp,
    orientation = "horizontal",
    children,
    className,
    asChild,
    disabled,
    invalid,
    required,
    ...rootProps
  } = props;
 
  const dir = useDirection(dirProp);
 
  const contextValue = React.useMemo<SegmentedInputContextValue>(
    () => ({
      dir,
      orientation,
      size,
      disabled,
      invalid,
      required,
    }),
    [dir, orientation, size, disabled, invalid, required],
  );
 
  const childrenArray = React.Children.toArray(children);
  const childrenCount = childrenArray.length;
 
  const segmentedInputItems = React.Children.map(children, (child, index) => {
    if (React.isValidElement<SegmentedInputItemProps>(child)) {
      if (!child.props.position) {
        let position: Position;
 
        if (childrenCount === 1) {
          position = "isolated";
        } else if (index === 0) {
          position = "first";
        } else if (index === childrenCount - 1) {
          position = "last";
        } else {
          position = "middle";
        }
 
        return React.cloneElement(child, { position });
      }
    }
    return child;
  });
 
  const RootPrimitive = asChild ? Slot : "div";
 
  return (
    <SegmentedInputContext.Provider value={contextValue}>
      <RootPrimitive
        role="group"
        aria-orientation={orientation}
        data-slot="segmented-input"
        data-orientation={orientation}
        data-disabled={disabled ? "" : undefined}
        data-invalid={invalid ? "" : undefined}
        data-required={required ? "" : undefined}
        dir={dir}
        {...rootProps}
        className={cn(
          "flex",
          orientation === "horizontal" ? "flex-row" : "flex-col",
          className,
        )}
      >
        {segmentedInputItems}
      </RootPrimitive>
    </SegmentedInputContext.Provider>
  );
}
 
const segmentedInputItemVariants = cva("", {
  variants: {
    position: {
      isolated: "",
      first: "rounded-e-none",
      middle: "-ms-px rounded-none border-l-0",
      last: "-ms-px rounded-s-none border-l-0",
    },
    orientation: {
      horizontal: "",
      vertical: "",
    },
    size: {
      sm: "h-8 px-2 text-xs",
      default: "h-9 px-3",
      lg: "h-11 px-4",
    },
  },
  compoundVariants: [
    {
      position: "first",
      orientation: "vertical",
      class: "ms-0 rounded-e-md rounded-b-none border-l",
    },
    {
      position: "middle",
      orientation: "vertical",
      class: "-mt-px ms-0 rounded-none border-t-0 border-l",
    },
    {
      position: "last",
      orientation: "vertical",
      class: "-mt-px ms-0 rounded-s-md rounded-t-none border-t-0 border-l",
    },
  ],
  defaultVariants: {
    position: "isolated",
    orientation: "horizontal",
    size: "default",
  },
});
 
interface SegmentedInputItemProps
  extends React.ComponentProps<"input">,
    Omit<VariantProps<typeof segmentedInputItemVariants>, "size"> {
  asChild?: boolean;
}
 
function SegmentedInputItem(props: SegmentedInputItemProps) {
  const { asChild, className, position, disabled, required, ...inputProps } =
    props;
  const context = useSegmentedInputContext(ITEM_NAME);
 
  const isDisabled = disabled ?? context.disabled;
  const isRequired = required ?? context.required;
 
  const ItemPrimitive = asChild ? Slot : Input;
 
  return (
    <ItemPrimitive
      aria-invalid={context.invalid}
      aria-required={isRequired}
      data-disabled={isDisabled ? "" : undefined}
      data-invalid={context.invalid ? "" : undefined}
      data-orientation={context.orientation}
      data-position={position}
      data-required={isRequired ? "" : undefined}
      data-slot="segmented-input-item"
      disabled={isDisabled}
      required={isRequired}
      {...inputProps}
      className={cn(
        segmentedInputItemVariants({
          position,
          orientation: context.orientation,
          size: context.size,
          className,
        }),
      )}
    />
  );
}
 
export { SegmentedInput, SegmentedInputItem };

Layout

Import the parts, and compose them together.

import { SegmentedInput, SegmentedInputItem } from "@/components/ui/segmented-input";

return (
  <SegmentedInput>
    <SegmentedInputItem />
  </SegmentedInput>
)

Examples

Form Input

Use segmented inputs for structured form data like phone numbers or addresses.

"use client";
 
import * as React from "react";
import { Button } from "@/components/ui/button";
import {
  SegmentedInput,
  SegmentedInputItem,
} from "@/components/ui/segmented-input";
 
export function SegmentedInputFormDemo() {
  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>
        <SegmentedInput
          className="w-full max-w-sm"
          aria-label="Phone number input"
        >
          <SegmentedInputItem
            placeholder="+1"
            value={phoneNumber.countryCode}
            onChange={(event) =>
              setPhoneNumber((prev) => ({
                ...prev,
                countryCode: event.target.value,
              }))
            }
            className="w-16"
            aria-label="Country code"
          />
          <SegmentedInputItem
            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"
          />
          <SegmentedInputItem
            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"
          />
        </SegmentedInput>
      </div>
      <Button type="submit" size="sm">
        Submit
      </Button>
    </form>
  );
}

RGB Color Input

Create color input controls using segmented inputs for RGB values.

"use client";
 
import * as React from "react";
import {
  SegmentedInput,
  SegmentedInputItem,
} from "@/components/ui/segmented-input";
 
export function SegmentedInputRgbDemo() {
  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>
      <SegmentedInput className="w-fit" aria-label="RGB color input">
        <SegmentedInputItem
          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)"
        />
        <SegmentedInputItem
          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)"
        />
        <SegmentedInputItem
          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)"
        />
      </SegmentedInput>
    </div>
  );
}

Vertical Layout

Display segmented inputs in a vertical orientation.

"use client";
 
import * as React from "react";
import {
  SegmentedInput,
  SegmentedInputItem,
} from "@/components/ui/segmented-input";
 
export function SegmentedInputVerticalDemo() {
  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>
        <SegmentedInput
          aria-label="Mailing address input"
          className="w-full max-w-sm"
          orientation="vertical"
        >
          <SegmentedInputItem
            aria-label="Street address"
            placeholder="Street Address"
            value={address.street}
            onChange={onFieldChange("street")}
          />
          <SegmentedInputItem
            aria-label="City"
            placeholder="City"
            value={address.city}
            onChange={onFieldChange("city")}
          />
          <SegmentedInputItem
            aria-label="ZIP code"
            placeholder="ZIP Code"
            value={address.zipCode}
            onChange={onFieldChange("zipCode")}
          />
        </SegmentedInput>
      </div>
      <p className="text-muted-foreground text-sm">
        Use arrow keys (up/down) to navigate between fields in vertical
        orientation.
      </p>
    </div>
  );
}

API Reference

SegmentedInput

The main segmented input container.

Prop

Type

SegmentedInputItem

Individual input items within the segmented input.

Prop

Type

Accessibility

The SegmentedInput 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 segment.
ShiftTabMoves focus to the previous input in the segment.