About this course
<p>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.</p>
<p>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.</p>
<p>Every one of these is available as a <a href="https://shadcnspace.com/components/date-picker"><strong>Shadcn Date Picker</strong></a> component you can install with a single CLI command instead of building from scratch.</p>
<p>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.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-youll-build">What You'll Build</a></p>
</li>
<li><p><a href="#heading-how-to-install-a-shadcn-date-time-picker">How to Install a Shadcn Date Time Picker</a></p>
</li>
<li><p><a href="#heading-how-to-build-a-date-and-time-picker">How to Build a Date and Time Picker</a></p>
</li>
<li><p><a href="#heading-how-to-build-a-date-range-picker">How to Build a Date Range Picker</a></p>
</li>
<li><p><a href="#heading-how-to-build-a-time-picker">How to Build a Time Picker</a></p>
</li>
<li><p><a href="#heading-live-preview-of-the-components">Live Preview of the components</a></p>
</li>
<li><p><a href="#heading-key-concepts-recap">Key Concepts Recap</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-resources">Resources</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along, you should know:</p>
<ul>
<li><p>The basics of React, including <code>useState</code> and props</p>
</li>
<li><p>How to install components with the shadcn/ui CLI</p>
</li>
<li><p>Basic Tailwind CSS class names</p>
</li>
</ul>
<p>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.</p>
<h2 id="heading-what-youll-build">What You'll Build</h2>
<ul>
<li><p>A <code>DateTimePicker</code> component that combines a calendar and time slots into one value.</p>
</li>
<li><p>A <code>TimePicker</code> variant that reuses the same time-slot logic without a calendar.</p>
</li>
<li><p>A <code>DateRangePicker</code> that lets a user pick a start and end date.</p>
</li>
</ul>
<h2 id="heading-how-to-install-a-shadcn-date-time-picker"><strong>How to Install a Shadcn Date Time Picker</strong></h2>
<p>All the components below install through the same CLI pattern. Pick the package manager you use:</p>
<p><strong>pnpm</strong></p>
<pre><code class="language-javascript">pnpm dlx shadcn@latest add @shadcn-space/date-picker-01
</code></pre>
<p><strong>npm</strong></p>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/date-picker-01
</code></pre>
<p><strong>yarn</strong></p>
<pre><code class="language-javascript">yarn dlx shadcn@latest add @shadcn-space/date-picker-01
</code></pre>
<p><strong>bun</strong></p>
<pre><code class="language-javascript">bunx --bun shadcn@latest add @shadcn-space/date-picker-01
</code></pre>
<p>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 <a href="https://shadcnspace.com/docs/getting-started/how-to-use-shadcn-cli"><strong>getting-started guide</strong></a> covers that first and shows how to integrate these components when you're working through an MCP-connected editor.</p>
<h2 id="heading-how-to-build-a-date-and-time-picker"><strong>How to Build a Date and Time Picker</strong></h2>
<p>This is the combined picker: a calendar popover for the date, plus start and end time fields, wrapped around a booking confirmation flow.</p>
<p><strong>Folder structure:</strong></p>
<pre><code class="language-javascript">components
└── shadcn-space
└── date-picker
└── date-picker-01.tsx
</code></pre>
<p><strong>Component code:</strong></p>
<pre><code class="language-javascript">"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;
</code></pre>
<h3 id="heading-how-this-component-works">How This Component Works</h3>
<p>This component combines three pieces of state into a simple booking flow:</p>
<ul>
<li><p>The selected date</p>
</li>
<li><p>Start and end times</p>
</li>
<li><p>The booking status (idle, loading, success)</p>
</li>
</ul>
<p>The date is stored using React state:</p>
<pre><code class="language-javascript">const [date, setDate] = useState<Date | undefined>(undefined);
</code></pre>
<p>When a user selects a day from the calendar, the <code>onSelect</code> callback updates the state and closes the popover:</p>
<pre><code class="language-javascript">onSelect={(d) => {
setDate(d);
setOpen(false);
}}
</code></pre>
<p>The calendar itself lives inside a <code>Popover</code>, which keeps the UI compact. Clicking the trigger button opens the calendar panel:</p>
<pre><code class="language-plaintext"><Popover open={open} onOpenChange={setOpen}>
</code></pre>
<p>The displayed date uses <code>date-fns</code> formatting:</p>
<pre><code class="language-plaintext">format(date, "PPP")
</code></pre>
<p>This converts a JavaScript <code>Date</code> object into a readable format such as:</p>
<pre><code class="language-plaintext">July 22, 2026
</code></pre>
<p>The time fields use native HTML inputs:</p>
<pre><code class="language-plaintext"><Input type="time" />
</code></pre>
<p>Native time inputs provide built-in browser support, mobile pickers, keyboard accessibility, and locale-aware formatting without additional libraries.</p>
<p>The booking button demonstrates how UI state can change during an async action:</p>
<pre><code class="language-plaintext">"idle" → "loading" → "success"
</code></pre>
<p>In a real application, <code>handleBooking()</code> would typically call an API endpoint instead of using <code>setTimeout</code>.</p>
<p>This pattern works well for:</p>
<ul>
<li><p>Meeting schedulers</p>
</li>
<li><p>Appointment systems</p>
</li>
<li><p>Interview booking tools</p>
</li>
<li><p>Event registration forms</p>
</li>
<li><p>SaaS scheduling workflows</p>
</li>
</ul>
<h2 id="heading-how-to-build-a-date-range-picker"><strong>How to Build a Date Range Picker</strong></h2>
<p>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.</p>
<p><strong>Folder structure:</strong></p>
<pre><code class="language-javascript">components
└── shadcn-space
└── date-picker
└── date-picker-02.tsx
</code></pre>
<p>Install this Shadcn Date Range Picker with:</p>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/date-picker-02
</code></pre>
<p><strong>Component code:</strong></p>
<pre><code class="language-javascript">"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;
</code></pre>
<h3 id="heading-how-the-date-range-picker-works">How the Date Range Picker Works</h3>
<p>This version switches the Calendar component from single-date mode to range mode:</p>
<pre><code class="language-javascript">mode="range"
</code></pre>
<p>Instead of storing one <code>Date</code>, the component stores a <code>DateRange</code> object:</p>
<pre><code class="language-javascript">{
from: Date,
to: Date
}
</code></pre>
<p>This makes it easy to work with booking systems, hotel stays, travel forms, and rental applications.</p>
<p>The component displays two calendar months:</p>
<pre><code class="language-javascript">numberOfMonths={2}
</code></pre>
<p>Showing two months reduces navigation and improves the selection experience for longer stays.</p>
<p>The stay duration is calculated directly from the selected dates:</p>
<pre><code class="language-javascript">(date.to.getTime() - date.from.getTime())
</code></pre>
<p>This avoids additional libraries and keeps the logic simple.</p>
<p>Compared to the first example:</p>
<ul>
<li><p>Uses <code>DateRange</code> instead of a single <code>Date</code></p>
</li>
<li><p>Uses <code>mode="range"</code></p>
</li>
<li><p>Displays two months</p>
</li>
<li><p>Adds derived data like total nights</p>
</li>
</ul>
<h2 id="heading-how-to-build-a-time-picker"><strong>How to Build a Time Picker</strong></h2>
<p>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.</p>
<p><strong>Folder structure:</strong></p>
<pre><code class="language-javascript">components
└── shadcn-space
└── date-picker
└── date-picker-03.tsx
</code></pre>
<p>Install this Shadcn Time Picker with:</p>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/date-picker-03
</code></pre>
<p><strong>Component code:</strong></p>
<pre><code class="language-javascript">"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;
</code></pre>
<h3 id="heading-how-the-time-picker-works">How the Time Picker Works</h3>
<p>This component uses the browser's native time picker instead of building a custom dropdown.</p>
<p>The input is referenced with <code>useRef</code>:</p>
<pre><code class="language-javascript">const inputRef = useRef<HTMLInputElement>(null);
</code></pre>
<p>This allows the icon button to access the input element directly.</p>
<p>When users click the clock icon, the browser's picker opens programmatically:</p>
<pre><code class="language-javascript">inputRef.current.showPicker();
</code></pre>
<p>The <code>showPicker()</code> method is a modern browser API that opens the same interface users would see if they clicked the input manually.</p>
<p>The component wraps the input inside an <code>InputGroup</code>:</p>
<pre><code class="language-plaintext"><InputGroup>
</code></pre>
<p>This creates a cleaner layout where the icon behaves as part of the field rather than a separate button.</p>
<p>The browser's default picker icon is hidden:</p>
<pre><code class="language-plaintext">[&::-webkit-calendar-picker-indicator]:hidden
</code></pre>
<p>This prevents duplicate icons and gives full control over the UI.</p>
<p>The benefit of this approach is that it keeps:</p>
<ul>
<li><p>Native accessibility</p>
</li>
<li><p>Mobile keyboard support</p>
</li>
<li><p>Locale-aware formatting</p>
</li>
<li><p>Better browser compatibility</p>
</li>
</ul>
<p>Instead of rebuilding time selection from scratch, the component improves the native experience with custom styling.</p>
<h2 id="heading-live-preview-of-the-components"><strong>Live Preview of the Components</strong></h2>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/f815455d-9f2d-4ad1-ac9c-155c37aae094.gif" alt="f815455d-9f2d-4ad1-ac9c-155c37aae094" style="display:block;margin:0 auto" width="1152" height="648" loading="lazy">
<h2 id="heading-key-concepts-recap"><strong>Key Concepts Recap</strong></h2>
<ul>
<li><p><strong>Pick the pattern that matches the data you're collecting</strong>: A single date and time for bookings, a range for stays, a bare time field for slots.</p>
</li>
<li><p><strong>Every component installs the same way</strong>: One CLI command per component: <code>pnpm</code>, <code>npm</code>, <code>yarn</code>, and <code>bun</code>. All are supported. Adding a new picker to a project takes one line, not a manual build.</p>
</li>
<li><p><strong>The date and time picker separates date state from time state</strong>, then combines them at the confirmation step. This keeps the calendar and the time inputs from getting out of sync.</p>
</li>
<li><p><strong>The range picker leans on the Calendar's built-in</strong> <code>mode="range"</code>: The two-month view and the night count come from the selection state, not custom range math.</p>
</li>
<li><p><strong>The time picker wraps the native</strong> <code><input type="time"></code> <strong>instead of replacing it</strong>: This keeps native accessibility and mobile keyboard behavior intact.</p>
</li>
</ul>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>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.</p>
<p>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.</p>
<h2 id="heading-resources"><strong>Resources</strong></h2>
<ul>
<li><p><a href="https://ui.shadcn.com/docs"><strong>shadcn/ui documentation</strong></a>: For the official Popover, Calendar, and Button component APIs</p>
</li>
<li><p><a href="https://date-fns.org/docs/Getting-Started"><strong>date-fns documentation</strong></a>: For the date helper functions and locale files used in this tutorial</p>
</li>
<li><p><a href="https://daypicker.dev/"><strong>react-day-picker documentation</strong></a>: For the range and multi-date selection modes that power the shadcn/ui Calendar</p>
</li>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions"><strong>MDN: ARIA live regions</strong></a>: For more on how <code>role="alert"</code> works</p>
</li>
<li><p><a href="https://shadcnspace.com/components/date-picker"><strong>Shadcn Date Time Picker components</strong></a>: If you want more prebuilt date and time picker variants to compare against.</p>
</li>
<li><p><a href="https://shadcnspace.com/components"><strong>Full Shadcn components library</strong></a>: For other shadcn/ui-compatible components</p>
</li>
<li><p><a href="https://wrappixel.com/blog/shadcn-date-picker"><strong>Guide to free Shadcn date and time pickers</strong></a></p>
</li>
</ul>