A date and time picker is one of those components that looks small in a design file and turns into a real time sink once you start building it. You need a calendar, a time selector, a state that keeps both in sync, and usually a range mode and a translated version somewhere down the line, too.
This guide walks through ready-made picker patterns you can drop into a React project today: a combined date and time picker, a date range picker, and a time picker.
Every one of these is available as a Shadcn Date Picker component you can install with a single CLI command instead of building from scratch.
These components are built on both Radix and Base UI primitives, and the versions below use Base UI. They also support copy-prompt functionality, so you can paste them straight into v0, Lovable, or Bolt if that's part of your workflow.
Table of Contents
Prerequisites
To follow along, you should know:
The basics of React, including
useStateand propsHow to install components with the shadcn/ui CLI
Basic Tailwind CSS class names
You also need a React project with shadcn/ui already set up. If you haven't done that yet, run the shadcn/ui CLI setup command in your project before continuing.
What You'll Build
A
DateTimePickercomponent that combines a calendar and time slots into one value.A
TimePickervariant that reuses the same time-slot logic without a calendar.A
DateRangePickerthat lets a user pick a start and end date.
How to Install a Shadcn Date Time Picker
All the components below install through the same CLI pattern. Pick the package manager you use:
pnpm
pnpm dlx shadcn@latest add @shadcn-space/date-picker-01
npm
npx shadcn@latest add @shadcn-space/date-picker-01
yarn
yarn dlx shadcn@latest add @shadcn-space/date-picker-01
bun
bunx --bun shadcn@latest add @shadcn-space/date-picker-01
Every other component below installs the same way: just swap the package name at the end of the command. If you haven't set up the CLI in your project yet, this getting-started guide covers that first and shows how to integrate these components when you're working through an MCP-connected editor.
How to Build a Date and Time Picker
This is the combined picker: a calendar popover for the date, plus start and end time fields, wrapped around a booking confirmation flow.
Folder structure:
components
└── shadcn-space
└── date-picker
└── date-picker-01.tsx
Component code:
"use client";
import { useState } from "react";
import { format } from "date-fns";
import { CalendarIcon, Clock, ChevronDown, Check } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
const DateAndTimePickerDemo = () => {
const [open, setOpen] = useState(false);
const [date, setDate] = useState<Date | undefined>(undefined);
const [bookingStatus, setBookingStatus] = useState<
"idle" | "loading" | "success"
>("idle");
const handleBooking = () => {
setBookingStatus("loading");
setTimeout(() => setBookingStatus("success"), 1500);
};
return (
<>
<div className="grid gap-6">
<div className="grid gap-2">
<Label htmlFor="date" className="text-sm font-semibold">
Select Date
</Label>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
onPointerDown={() => setBookingStatus("idle")}
render={
<Button
variant="outline"
id="date"
className={cn(
"w-full justify-start text-left font-normal h-10 transition-all hover:bg-muted/50 cursor-pointer",
!date && "text-muted-foreground",
)}
>
<CalendarIcon className="mr-2 h-4 w-4 opacity-70" />
{date ? format(date, "PPP") : <span>Select a date</span>}
<ChevronDown className="ml-auto h-4 w-4 opacity-50" />
</Button>
}
/>
<PopoverContent
className="w-auto p-0 border-muted-foreground/10 shadow-2xl"
align="start"
>
<Calendar
mode="single"
selected={date}
onSelect={(d) => {
setDate(d);
setOpen(false);
}}
className="rounded-md border-none"
/>
</PopoverContent>
</Popover>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label
htmlFor="time-from"
className="text-sm font-semibold text-muted-foreground flex items-center gap-1.5"
>
<Clock className="size-3.5" /> Start Time
</Label>
<Input
type="time"
id="time-from"
defaultValue="09:00"
className="h-10 bg-background appearance-none transition-all focus:ring-2 focus:ring-primary/20"
/>
</div>
<div className="grid gap-2">
<Label
htmlFor="time-to"
className="text-sm font-semibold text-muted-foreground flex items-center gap-1.5"
>
<Clock className="size-3.5" /> End Time
</Label>
<Input
type="time"
id="time-to"
defaultValue="10:00"
className="h-10 bg-background appearance-none transition-all focus:ring-2 focus:ring-primary/20"
/>
</div>
</div>
<Button
className="w-full h-11 font-semibold transition-all group overflow-hidden relative cursor-pointer"
onClick={handleBooking}
disabled={!date || bookingStatus !== "idle"}
>
{bookingStatus === "idle" && (
<span className="flex items-center gap-2">Confirm Meet</span>
)}
{bookingStatus === "loading" && (
<div className="flex items-center gap-2">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
Processing...
</div>
)}
{bookingStatus === "success" && (
<span className="flex items-center gap-2 animate-in zoom-in-50 duration-300">
<Check className="h-4 w-4" />
Meet Scheduled!
</span>
)}
</Button>
</div>
</>
);
};
export default DateAndTimePickerDemo;
How This Component Works
This component combines three pieces of state into a simple booking flow:
The selected date
Start and end times
The booking status (idle, loading, success)
The date is stored using React state:
const [date, setDate] = useState<Date | undefined>(undefined);
When a user selects a day from the calendar, the onSelect callback updates the state and closes the popover:
onSelect={(d) => {
setDate(d);
setOpen(false);
}}
The calendar itself lives inside a Popover, which keeps the UI compact. Clicking the trigger button opens the calendar panel:
<Popover open={open} onOpenChange={setOpen}>
The displayed date uses date-fns formatting:
format(date, "PPP")
This converts a JavaScript Date object into a readable format such as:
July 22, 2026
The time fields use native HTML inputs:
<Input type="time" />
Native time inputs provide built-in browser support, mobile pickers, keyboard accessibility, and locale-aware formatting without additional libraries.
The booking button demonstrates how UI state can change during an async action:
"idle" → "loading" → "success"
In a real application, handleBooking() would typically call an API endpoint instead of using setTimeout.
This pattern works well for:
Meeting schedulers
Appointment systems
Interview booking tools
Event registration forms
SaaS scheduling workflows
How to Build a Date Range Picker
For anything involving a stay, a rental, or a multi-day booking, you need a range instead of a single date. This component pairs a two-month calendar view with a formatted range label and a computed night count.
Folder structure:
components
└── shadcn-space
└── date-picker
└── date-picker-02.tsx
Install this Shadcn Date Range Picker with:
npx shadcn@latest add @shadcn-space/date-picker-02
Component code:
"use client";
import * as React from "react";
import { CalendarIcon, ChevronDown } from "lucide-react";
import { addDays, format } from "date-fns";
import { DateRange } from "react-day-picker";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Label } from "@/components/ui/label";
const DateRangePickerDemo = () => {
const [date, setDate] = React.useState<DateRange | undefined>({
from: new Date(),
to: addDays(new Date(), 7),
});
return (
<div className="grid gap-3 max-w-sm mx-auto">
<Label htmlFor="date-range" className="text-sm font-medium px-1">
Select Travel Dates
</Label>
<div className={cn("grid gap-2")}>
<Popover>
<PopoverTrigger
render={
<Button
id="date-range"
variant={"outline"}
className={cn(
"w-full justify-start text-left font-normal h-11 transition-all hover:bg-muted/50 focus:ring-2 focus:ring-primary/20 cursor-pointer",
!date && "text-muted-foreground",
)}
>
<CalendarIcon className="mr-2 h-4 w-4 opacity-70" />
{date?.from ? (
date.to ? (
<>
{format(date.from, "LLL dd, y")} -{" "}
{format(date.to, "LLL dd, y")}
</>
) : (
format(date.from, "LLL dd, y")
)
) : (
<span>Pick a range</span>
)}
<ChevronDown className="ml-auto h-4 w-4 opacity-50" />
</Button>
}
/>
<PopoverContent
className="w-auto p-0 border-muted/20 shadow-xl"
align="start"
>
<Calendar
mode="range"
defaultMonth={date?.from}
selected={date}
onSelect={setDate}
numberOfMonths={2}
className="p-3"
/>
</PopoverContent>
</Popover>
</div>
<p className="text-xs text-muted-foreground px-1">
{date?.from && date?.to
? `Stay duration: ${Math.round((date.to.getTime() - date.from.getTime()) / (1000 * 60 * 60 * 24))} nights`
: "Please select a valid date range."}
</p>
</div>
);
};
export default DateRangePickerDemo;
How the Date Range Picker Works
This version switches the Calendar component from single-date mode to range mode:
mode="range"
Instead of storing one Date, the component stores a DateRange object:
{
from: Date,
to: Date
}
This makes it easy to work with booking systems, hotel stays, travel forms, and rental applications.
The component displays two calendar months:
numberOfMonths={2}
Showing two months reduces navigation and improves the selection experience for longer stays.
The stay duration is calculated directly from the selected dates:
(date.to.getTime() - date.from.getTime())
This avoids additional libraries and keeps the logic simple.
Compared to the first example:
Uses
DateRangeinstead of a singleDateUses
mode="range"Displays two months
Adds derived data like total nights
How to Build a Time Picker
Not every form needs a calendar. This one wraps the native in an InputGroup with a clock icon that triggers the browser's own time picker UI on click.
Folder structure:
components
└── shadcn-space
└── date-picker
└── date-picker-03.tsx
Install this Shadcn Time Picker with:
npx shadcn@latest add @shadcn-space/date-picker-03
Component code:
"use client";
import { useRef } from "react";
import { Label } from "@/components/ui/label";
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/components/ui/input-group";
import { Clock8Icon } from "lucide-react";
const TimePickerWithIconDemo = () => {
const inputRef = useRef<HTMLInputElement>(null);
const handleShowPicker = () => {
if (inputRef.current && "showPicker" in inputRef.current) {
try {
inputRef.current.showPicker();
} catch (error) {
console.error("Failed to open native picker:", error);
}
}
};
return (
<div className="flex w-full max-w-xs flex-col gap-2">
<Label htmlFor="time-picker">Select Slot</Label>
<InputGroup>
<InputGroupAddon
align="inline-start"
className="cursor-pointer hover:text-foreground transition-colors"
onClick={handleShowPicker}
title="Open time picker"
>
<Clock8Icon className="size-4" />
</InputGroupAddon>
<InputGroupInput
ref={inputRef}
type="time"
id="time-picker"
step="1"
defaultValue="08:30:00"
className="appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
/>
</InputGroup>
</div>
);
};
export default TimePickerWithIconDemo;
How the Time Picker Works
This component uses the browser's native time picker instead of building a custom dropdown.
The input is referenced with useRef:
const inputRef = useRef<HTMLInputElement>(null);
This allows the icon button to access the input element directly.
When users click the clock icon, the browser's picker opens programmatically:
inputRef.current.showPicker();
The showPicker() method is a modern browser API that opens the same interface users would see if they clicked the input manually.
The component wraps the input inside an InputGroup:
<InputGroup>
This creates a cleaner layout where the icon behaves as part of the field rather than a separate button.
The browser's default picker icon is hidden:
[&::-webkit-calendar-picker-indicator]:hidden
This prevents duplicate icons and gives full control over the UI.
The benefit of this approach is that it keeps:
Native accessibility
Mobile keyboard support
Locale-aware formatting
Better browser compatibility
Instead of rebuilding time selection from scratch, the component improves the native experience with custom styling.
Live Preview of the Components
Key Concepts Recap
Pick the pattern that matches the data you're collecting: A single date and time for bookings, a range for stays, a bare time field for slots.
Every component installs the same way: One CLI command per component:
pnpm,npm,yarn, andbun. All are supported. Adding a new picker to a project takes one line, not a manual build.The date and time picker separates date state from time state, then combines them at the confirmation step. This keeps the calendar and the time inputs from getting out of sync.
The range picker leans on the Calendar's built-in
mode="range": The two-month view and the night count come from the selection state, not custom range math.The time picker wraps the native
<input type="time">instead of replacing it: This keeps native accessibility and mobile keyboard behavior intact.
Conclusion
You now have working picker patterns for React: a date and time picker for bookings, a date range picker for stays, and a plain time picker for slots.
Each one installs with a single CLI command, so you can pick whichever one matches the form you're building and have it running in minutes.
Resources
shadcn/ui documentation: For the official Popover, Calendar, and Button component APIs
date-fns documentation: For the date helper functions and locale files used in this tutorial
react-day-picker documentation: For the range and multi-date selection modes that power the shadcn/ui Calendar
MDN: ARIA live regions: For more on how
role="alert"worksShadcn Date Time Picker components: If you want more prebuilt date and time picker variants to compare against.
Full Shadcn components library: For other shadcn/ui-compatible components