Components
"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-authorityCopy 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
| Key | Description |
|---|---|
| Tab | Moves focus to the next input in the segment. |
| ShiftTab | Moves focus to the previous input in the segment. |