Shadcn Date Input for React and Tailwind
Segmented input for dates and times.
Installation
bunx --bun shadcn@latest add https://kit.dev/r/date-input.jsonpnpm dlx shadcn@latest add https://kit.dev/r/date-input.jsonnpx shadcn@latest add https://kit.dev/r/date-input.jsonyarn shadcn@latest add https://kit.dev/r/date-input.json<Step>This component depends on Input Group. Install it first if you haven't already.</Step>
Install the following dependencies:
bun add @ark-ui/react lucide-reactpnpm add @ark-ui/react lucide-reactnpm install @ark-ui/react lucide-reactyarn add @ark-ui/react lucide-reactCopy and paste the following code into your project.
"use client";
import {
DateInput as ArkDateInput,
useDateInput as useArkDateInput,
useDateInputContext as useArkDateInputContext,
} from "@ark-ui/react/date-input";
import { XIcon } from "lucide-react";
import type React from "react";
import { cn } from "@/lib/utils";
import type { InputProps } from "@/components/ui/input";
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
type InputGroupProps,
} from "@/components/ui/input-group";
export const useDateInput: typeof useArkDateInput = (props) =>
useArkDateInput({
shouldForceLeadingZeros: true,
...props,
});
export const useDateInputContext = useArkDateInputContext;
export const DateInputContext: typeof ArkDateInput.Context =
ArkDateInput.Context;
interface DateInputLayoutProps {
/**
* The selection mode. Must match the machine option.
*
* @default "single"
*/
selectionMode?: "single" | "range";
/**
* The separator to show between the date input fields in range mode.
*
* @default "-"
*/
separator?: string | React.ReactNode;
/**
* Whether to show the clear button.
*
* @default false
*/
showClear?: boolean;
/**
* The size of the control.
*
* @default "md"
*/
size?: InputProps["size"];
}
interface DateInputProps
extends React.ComponentProps<typeof ArkDateInput.Root>,
DateInputLayoutProps {}
interface DateInputControlProps extends Pick<InputGroupProps, "size"> {
children: React.ReactNode;
showClear: boolean;
}
const dateInputRootClassName = cn(
"group/date-input",
"flex w-full flex-col items-start gap-2"
);
export const DateInput = (props: DateInputProps) => {
const {
size = "md",
showClear = false,
selectionMode = "single",
shouldForceLeadingZeros = true,
separator = "-",
className,
children,
...rest
} = props;
return (
<ArkDateInput.Root
className={cn(dateInputRootClassName, className)}
data-size={size}
data-slot="date-input"
selectionMode={selectionMode}
shouldForceLeadingZeros={shouldForceLeadingZeros}
{...rest}
>
{children}
<DateInputFields
selectionMode={selectionMode}
separator={separator}
showClear={showClear}
size={size}
/>
</ArkDateInput.Root>
);
};
export const DateInputRootProvider = (
props: React.ComponentProps<typeof ArkDateInput.RootProvider> &
DateInputLayoutProps
) => {
const {
size = "md",
showClear = false,
selectionMode = "single",
separator = "-",
className,
children,
...rest
} = props;
return (
<ArkDateInput.RootProvider
className={cn(dateInputRootClassName, className)}
data-size={size}
data-slot="date-input"
{...rest}
>
{children}
<DateInputFields
selectionMode={selectionMode}
separator={separator}
showClear={showClear}
size={size}
/>
</ArkDateInput.RootProvider>
);
};
export const DateInputLabel = (
props: React.ComponentProps<typeof ArkDateInput.Label>
) => {
const { className, ...rest } = props;
return (
<ArkDateInput.Label
className={cn("font-medium text-sm", className)}
data-slot="date-input-label"
{...rest}
/>
);
};
const DateInputFields = (
props: Required<Pick<DateInputLayoutProps, "showClear" | "selectionMode">> &
Pick<DateInputLayoutProps, "separator" | "size">
) => {
const { size = "md", showClear, separator = "-", selectionMode } = props;
return (
<>
<DateInputControl showClear={showClear} size={size}>
<DateInputSegmentGroup index={0} />
{selectionMode === "range" && (
<>
<span
aria-hidden="true"
className="pointer-events-none select-none text-muted-foreground [&_svg]:size-3 [&_svg]:shrink-0"
>
{separator}
</span>
<DateInputSegmentGroup index={1} />
</>
)}
</DateInputControl>
<ArkDateInput.HiddenInput index={0} key="date-input-hidden-0" />
{selectionMode === "range" && (
<ArkDateInput.HiddenInput index={1} key="date-input-hidden-1" />
)}
</>
);
};
const DateInputControl = (props: DateInputControlProps) => {
const { size = "md", showClear, children } = props;
const dateInput = useDateInputContext();
const hasValue = dateInput.value.length > 0;
const handleClear = () => {
dateInput.clearValue();
};
return (
<ArkDateInput.Control asChild data-slot="date-input-control">
<InputGroup
className={cn(
"px-3",
"data-disabled:pointer-events-none data-disabled:opacity-64",
"has-data-[slot=date-input-clear]:pe-0"
)}
size={size}
>
<div
aria-invalid={dateInput.invalid || undefined}
className={cn(
"min-w-0",
"flex flex-1 items-center gap-2",
"text-base md:text-sm"
)}
data-slot="date-input-field"
>
{children}
</div>
{showClear && hasValue && !dateInput.disabled && (
<InputGroupAddon align="inline-end">
<InputGroupButton
aria-label="Clear date"
data-slot="date-input-clear"
onClick={handleClear}
size="icon-xs"
variant="ghost"
>
<XIcon aria-hidden="true" />
</InputGroupButton>
</InputGroupAddon>
)}
</InputGroup>
</ArkDateInput.Control>
);
};
const DateInputSegmentGroup = (
props: React.ComponentProps<typeof ArkDateInput.SegmentGroup>
) => {
const { index = 0, className, ...rest } = props;
return (
<ArkDateInput.SegmentGroup
className={cn("flex min-w-0 grow items-center gap-px", className)}
data-slot="date-input-segment-group"
index={index}
{...rest}
>
<ArkDateInput.SegmentContext>
{(segment) => <DateInputSegment segment={segment} />}
</ArkDateInput.SegmentContext>
</ArkDateInput.SegmentGroup>
);
};
const DateInputSegment = (
props: React.ComponentProps<typeof ArkDateInput.Segment>
) => {
const { className, ...rest } = props;
return (
<ArkDateInput.Segment
className={cn(
"tabular-nums",
"rounded-sm border-0 shadow-none ring-0",
"not-data-[type=literal]:px-0.5",
"not-data-[type=literal]:focus:bg-primary not-data-[type=literal]:focus:text-primary-foreground",
"data-[type=literal]:select-none data-[type=literal]:px-px data-[type=literal]:text-muted-foreground/64",
"data-placeholder-shown:text-muted-foreground/64",
"outline-none",
"data-readonly:cursor-default",
"group-aria-invalid/date-input:text-destructive group-data-invalid/date-input:text-destructive",
"not-data-[type=literal]:focus:group-data-invalid/date-input:bg-destructive not-data-[type=literal]:focus:group-data-invalid/date-input:text-white",
"dark:group-data-invalid/date-input:text-destructive-foreground dark:group-aria-invalid/date-input:text-destructive-foreground",
"dark:not-data-[type=literal]:focus:group-data-invalid/date-input:bg-destructive-foreground dark:not-data-[type=literal]:focus:group-data-invalid/date-input:text-white",
className
)}
data-slot="date-input-segment"
{...rest}
/>
);
};Update the import paths to match your project setup.
Anatomy
DateInput
├── DateInputLabel (optional)
└── Control (baked in)
├── SegmentGroup
│ └── Segment
├── separator (range)
├── SegmentGroup (range)
├── HiddenInput (baked in)
└── Clear (optional)shadcn.io Date Input wraps Ark Date Input. Control, segments, and hidden inputs are baked in. useDateInput is the machine hook for DateInputRootProvider; useDateInputContext / DateInputContext is in-tree.
Prefer Field for the visible label. For a calendar popup, use Date Picker. For an inline calendar, use Calendar.
Usage
import { parseDate } from "@ark-ui/react/date-picker";
import { DateInput } from "@/components/ui/date-input";<DateInput />value / defaultValue are always a DateValue[]. Build values with parseDate (from @ark-ui/react/date-picker or @internationalized/date).
shadcn.io defaults shouldForceLeadingZeros to true (Ark: false). selectionMode is "single". granularity is "day". Size is set on the control (sm h-7, md h-8, lg h-9).
Controlled
Control the value with value and onValueChange. { value: DateValue[]; valueAsString: string[] }.
Default value
Set the initial date with defaultValue and parseDate.
Root Provider
Use useDateInput with DateInputRootProvider when you need the API outside the tree. Pass machine options (defaultValue, granularity, selectionMode, …) to useDateInput(), not to the provider. Layout (size, showClear, separator, selectionMode for the second group) still goes on the provider.
States
Invalid
Disabled
Read-only
Sizes
Size is set on DateInput. sm is h-7, md is h-8, lg is h-9.
Small
Medium
Large
Examples
With Field
Field wires the label and helper text to the control.
Granularity
granularity is the smallest unit shown: day, hour, minute, or second.
Time only
Set granularity to minute or second and pass a formatter that only includes time fields. hourCycle is 12 or 24.
Range
Set selectionMode="range" for start and end groups. Customize the divider with separator (string or node).
Custom separator
Min and max
Restrict committed dates with min and max (DateValue).
Leading zeros
shadcn.io forces leading zeros by default. Set shouldForceLeadingZeros={false} to follow the locale.
With clear button
Pass showClear. The clear control is shown when there is a value.
Context
Read value and focus with DateInputContext or useDateInputContext.
Guides
Date Input vs Date Picker vs Calendar
| Date Input | Date Picker | Calendar | |
|---|---|---|---|
| Entry | Typed segments | Input + popup calendar | Inline calendar |
| Use when | Keyboard date/time, no popup | Pick from a calendar field | Always-visible calendar |
Values
Selected dates are always DateValue[] (CalendarDate, CalendarDateTime, or ZonedDateTime).
import { parseDate } from "@ark-ui/react/date-picker";
<DateInput defaultValue={[parseDate("2024-04-04")]} />Range:
<DateInput
defaultValue={[parseDate("2024-04-04"), parseDate("2024-04-10")]}
selectionMode="range"
/>onValueChange details: { value: DateValue[]; valueAsString: string[] }.
Time
granularity of hour / minute / second adds time segments. Pair with hourCycle and optionally useDateFormatter for a time-only field (no year/month/day in the formatter).
Root provider layout
selectionMode on DateInputRootProvider must match useDateInput({ selectionMode }) so the second group and hidden input render in range mode.
const dateInput = useDateInput({
selectionMode: "range",
});
<DateInputRootProvider selectionMode="range" value={dateInput} />API Reference
shadcn.io wraps Ark UI Date Input. Defaults below are shadcn.io Date Input values. shouldForceLeadingZeros is true here (Ark: false).
asChild merges props onto a single child element.
DateInput
Root. Renders a div. Bakes control, segments, hidden input, and optional clear.
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "md" | "lg" | "md" | Control height. sm is h-7, md is h-8, lg is h-9. |
showClear | boolean | false | Show a clear button when there is a value. |
separator | string | ReactNode | "-" | Divider between start and end groups in range mode. |
selectionMode | "single" | "range" | "single" | One date, or start and end. |
shouldForceLeadingZeros | boolean | true | Always pad month, day, and hour. |
granularity | "day" | "hour" | "minute" | "second" | "day" | Smallest unit shown. |
hourCycle | 12 | 24 | - | 12- or 24-hour clock. Locale default when omitted. |
formatter | DateFormatter | - | Custom formatter (for example time-only). |
value | DateValue[] | - | Controlled selected dates. |
defaultValue | DateValue[] | - | Uncontrolled initial dates. |
placeholderValue | DateValue | - | Controlled placeholder date used for empty segments. |
defaultPlaceholderValue | DateValue | - | Uncontrolled placeholder date. |
min | DateValue | - | Minimum committed date. |
max | DateValue | - | Maximum committed date. |
isDateUnavailable | (date: DateValue, locale: string) => boolean | - | Marks matching committed dates invalid. |
disabled | boolean | - | Disable the input. |
readOnly | boolean | - | Non-editable, still focusable. |
invalid | boolean | - | Mark as invalid. |
required | boolean | - | Mark as required. |
name | string | - | name on the hidden input. |
form | string | - | Associated form id. |
locale | string | "en-US" | BCP 47 locale for formatting. |
timeZone | string | "UTC" | Time zone. |
hideTimeZone | boolean | false | Hide the time zone segment on ZonedDateTime values. |
createCalendar | (identifier: CalendarIdentifier) => Calendar | - | Non-Gregorian calendars. |
translations | IntlTranslations | - | { placeholder?: (locale) => Record<EditableSegmentType, string> }. |
ids | Partial<{ root: string; label: (index: number) => string; control: string; segmentGroup: (index: number) => string; hiddenInput: (index: number) => string }> | - | Element ids for composition. |
id | string | - | Unique id for the machine. |
format | (date: DateValue, details: FormatDateDetails) => string | - | Custom string conversion. |
onValueChange | (details: ValueChangeDetails) => void | - | { value, valueAsString }. |
onPlaceholderChange | (details: PlaceholderChangeDetails) => void | - | Placeholder changed. { value, valueAsString, placeholderValue }. |
onFocusChange | (details: FocusChangeDetails) => void | - | { focused }. |
asChild | boolean | false | Render the child element instead of a div. |
className | string | - | Class names on the root. |
Children render above the baked control (for example DateInputLabel or DateInputContext).
| Attribute | Description |
|---|---|
data-slot | date-input |
data-scope | date-input |
data-part | root |
data-size | "sm", "md", or "lg" |
data-invalid | Present when invalid |
data-disabled | Present when disabled |
data-readonly | Present when read-only |
DateInputLabel
Optional visible label. Prefer Field (FieldLabel). Renders a label.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the label. |
| Attribute | Description |
|---|---|
data-slot | date-input-label |
data-scope | date-input |
data-part | label |
data-disabled | Present when disabled |
data-invalid | Present when invalid |
data-readonly | Present when read-only |
data-required | Present when required |
Control (baked in)
Input Group wrapping the segments and optional clear button. Not a separate export.
| Attribute | Description |
|---|---|
data-slot | date-input-control |
data-scope | date-input |
data-part | control |
data-disabled | Present when disabled |
data-invalid | Present when invalid |
data-readonly | Present when read-only |
The field wrapper is data-slot="date-input-field". The clear button is data-slot="date-input-clear" with aria-label="Clear date".
Segment group (baked in)
One group for single, two for range (index 0 and 1).
| Attribute | Description |
|---|---|
data-slot | date-input-segment-group |
data-scope | date-input |
data-part | segment-group |
Segment (baked in)
Each date part (year, month, day, hour, minute, second, dayPeriod, literal, …). Renders a span.
| Attribute | Description |
|---|---|
data-slot | date-input-segment |
data-scope | date-input |
data-part | segment |
data-type | Segment type (day, month, literal, …) |
data-placeholder-shown | Present when the segment is still a placeholder |
data-readonly | Present when read-only |
data-disabled | Present when disabled |
data-invalid | Present when invalid |
Hidden input (baked in)
One native input per group for form submission (index 0, and 1 in range mode).
| Attribute | Description |
|---|---|
data-scope | date-input |
data-part | hidden-input |
DateInputRootProvider
Root alternative that takes the API from useDateInput. Renders a div with the same baked control.
| Prop | Type | Default | Description |
|---|---|---|---|
value | UseDateInputReturn | required | Return value of useDateInput(). |
size | "sm" | "md" | "lg" | "md" | Control height. |
showClear | boolean | false | Show a clear button when there is a value. |
separator | string | ReactNode | "-" | Range divider. |
selectionMode | "single" | "range" | "single" | Must match useDateInput({ selectionMode }). |
asChild | boolean | false | Render the child element instead of a div. |
className | string | - | Class names on the root. |
Pass defaultValue, granularity, selectionMode, shouldForceLeadingZeros, and other machine options to useDateInput(), not to DateInputRootProvider. useDateInput already defaults shouldForceLeadingZeros to true.
| Attribute | Description |
|---|---|
data-slot | date-input |
data-scope | date-input |
data-part | root |
useDateInput
Creates the date input API for DateInputRootProvider. Same options as DateInput except layout-only props (size, showClear, separator). Applies shouldForceLeadingZeros: true unless you override it.
const dateInput = useDateInput({
defaultValue: [parseDate("2024-04-04")],
});
dateInput.focus();DateInputContext / useDateInputContext
Render-prop or hook access to date input state. Use inside DateInput or DateInputRootProvider.
| Property | Type | Description |
|---|---|---|
focused | boolean | Whether a segment is focused. |
disabled | boolean | Whether the input is disabled. |
invalid | boolean | Whether the input is invalid. |
value | DateValue[] | Selected dates. |
valueAsDate | Date[] | Selected dates as Date objects. |
valueAsString | string[] | Selected dates as strings. |
placeholderValue | DateValue | Placeholder date for empty segments. |
displayValues | IncompleteDate[] | Per-group editing state. |
focus | () => void | Focus the first segment. |
setValue | (values: DateValue[]) => void | Set the selection. |
clearValue | () => void | Clear the selection. |
getSegments | (props?: { index?: number }) => DateSegment[] | Segments for a group. |
getSegmentState | (props: { segment: DateSegment; index?: number }) => SegmentState | editable, focused, readonly for one segment. |
DateInputContext children: (context) => ReactNode.
Accessibility
Complies with the Date Picker WAI-ARIA spinbutton pattern on each editable segment. Label the control with Field (FieldLabel) or DateInputLabel. Hidden inputs submit the value with the form.
Keyboard support
| Key | Description |
|---|---|
ArrowLeft / ArrowRight | Move to the previous or next segment. In range mode, moves between start and end groups. |
ArrowUp / ArrowDown | Increment or decrement the focused segment. |
PageUp / PageDown | Larger step on the focused segment (for example 7 days, 15 minutes). |
Home / End | Jump to the minimum or maximum for the focused segment. |
| Digit keys | Type a value into the focused numeric segment. Advances when the field is full. |
Backspace / Delete | Clear the focused segment. |
Tab / Shift + Tab | Move between segments, then out of the control. |