UI/UI
Date Picker
A flexible date picker component with single date and date range selection modes, built with accessibility and responsive design in mind.
function BasicDatePickerExample() {
const [date, setDate] = React.useState<Date>();
return (
<DatePicker
value={date}
onChange={setDate}
placeholder="Select a date"
/>
);
}
Installation
Install following dependencies:
npm install class-variance-authority lucide-react motion react-dom
pnpm add class-variance-authority lucide-react motion react-dom
yarn add class-variance-authority lucide-react motion react-dom
bun add class-variance-authority lucide-react motion react-dom
Copy and paste the following code into your project.
"use client";
import * as React from "react";
import { Calendar } from "@/components/ui/calendar";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { CalendarIcon, ChevronDown } from "lucide-react";
import { motion, AnimatePresence } from "motion/react";
import { Button } from "@/components/ui/button";
import ReactDOM from "react-dom";
const datePickerVariants = cva(
"inline-flex h-9 w-full items-center justify-between rounded-[var(--radius)] border border-[hsl(var(--hu-border))] bg-[hsl(var(--hu-background))] px-3 py-2 text-sm font-medium text-[hsl(var(--hu-foreground))] transition-colors hover:bg-[hsl(var(--hu-accent))] hover:text-[hsl(var(--hu-accent-foreground))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--hu-ring))] disabled:cursor-not-allowed disabled:opacity-50 border border-[hsl(var(--hu-border))] bg-[hsl(var(--hu-input))]",
{
variants: {
variant: {
default: "",
outline: "border-2",
ghost: "border-transparent hover:border-[hsl(var(--hu-border))]",
},
size: {
sm: "h-7 sm:h-8 px-2 text-xs",
default: "h-8 sm:h-9 px-2 sm:px-3 text-xs sm:text-sm",
lg: "h-12 sm:h-10 px-3 sm:px-4 text-sm",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
interface DatePickerProps extends VariantProps<typeof datePickerVariants> {
value?: Date;
onChange?: (date: Date | undefined) => void;
placeholder?: string;
className?: string;
disabled?: boolean;
showIcon?: boolean;
minDate?: Date;
maxDate?: Date;
disabledDates?: (date: Date) => boolean;
locale?: string;
formatDate?: (date: Date) => string;
}
export function DatePicker({
value,
onChange,
placeholder = "Pick a date",
className,
disabled = false,
showIcon = true,
minDate,
maxDate,
disabledDates,
locale = "en-US",
formatDate,
variant,
size,
...props
}: DatePickerProps) {
const [isOpen, setIsOpen] = React.useState(false);
const [focusedDate, setFocusedDate] = React.useState(value || new Date());
const containerRef = React.useRef<HTMLDivElement>(null);
const [portalContainer, setPortalContainer] = React.useState<
Element | DocumentFragment | null
>(null);
React.useEffect(() => {
// Set portal container on client side only
if (typeof document !== "undefined") {
setPortalContainer(
document.getElementById("portal-root") || document.body
);
}
}, []);
const defaultFormatDate = (date: Date) => {
return date.toLocaleDateString(locale, {
year: "numeric",
month: "short",
day: "numeric",
});
};
const formatDateFn = formatDate || defaultFormatDate;
const handleSelect = (date: Date) => {
onChange?.(date);
setIsOpen(false);
};
const handleToggleOpen = () => {
setIsOpen(!isOpen);
};
const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
handleToggleOpen();
} else if (event.key === "Escape") {
setIsOpen(false);
}
};
React.useEffect(() => {
if (isOpen && typeof document !== "undefined") {
const originalOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = originalOverflow;
};
}
}, [isOpen]);
React.useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (typeof document === "undefined") return;
const target = event.target as Node;
const isClickInsideContainer =
containerRef.current && containerRef.current.contains(target);
// Check if click is inside any calendar popup using the data attribute
const calendarElement = document.querySelector(
'[data-datepicker-calendar="true"]'
);
const isClickInsideCalendar = calendarElement?.contains(target);
if (!isClickInsideContainer && !isClickInsideCalendar) {
setIsOpen(false);
}
};
if (isOpen && typeof document !== "undefined") {
document.addEventListener("mousedown", handleClickOutside);
return () =>
document.removeEventListener("mousedown", handleClickOutside);
}
}, [isOpen]);
React.useEffect(() => {
if (typeof document !== "undefined") {
const portalRoot = document.getElementById("portal-root");
if (!portalRoot) {
const newPortalRoot = document.createElement("div");
newPortalRoot.id = "portal-root";
newPortalRoot.style.position = "relative";
newPortalRoot.style.zIndex = "9999";
document.body.appendChild(newPortalRoot);
}
}
}, []);
const [calendarPosition, setCalendarPosition] = React.useState({
top: 0,
left: 0,
width: 0,
});
React.useEffect(() => {
if (isOpen && containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
setCalendarPosition({
top: rect.bottom + 8, // 8px margin
left: rect.left,
width: rect.width,
});
}
}, [isOpen]);
const calendarComponent = (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: -10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -10, scale: 0.95 }}
transition={{ duration: 0.2 }}
className="z-[9999] rounded-[var(--radius)] border border-[hsl(var(--hu-border))] bg-[hsl(var(--hu-background))] shadow-xl"
data-datepicker-calendar="true"
style={{
position: "fixed",
top: calendarPosition.top,
left: calendarPosition.left,
width: calendarPosition.width,
}}
>
<Calendar
selected={value}
onSelect={handleSelect}
minDate={minDate}
maxDate={maxDate}
disabled={disabledDates}
locale={locale}
alwaysOnTop={true}
/>
</motion.div>
)}
</AnimatePresence>
);
return (
<div className="relative" ref={containerRef} {...props}>
<Button
type="button"
onClick={handleToggleOpen}
onKeyDown={handleKeyDown}
disabled={disabled}
className={cn(datePickerVariants({ variant, size }), className)}
aria-expanded={isOpen}
aria-haspopup="dialog"
aria-label="Choose date"
>
<span className="flex items-center gap-2">
{showIcon && <CalendarIcon className="h-4 w-4 opacity-50" />}
<span
className={cn(!value && "text-[hsl(var(--hu-muted-foreground))]")}
>
{value ? formatDateFn(value) : placeholder}
</span>
</span>
<ChevronDown
className={cn(
"h-4 w-4 opacity-50 transition-transform duration-200",
isOpen && "rotate-180"
)}
/>
</Button>
{portalContainer &&
ReactDOM.createPortal(calendarComponent, portalContainer)}
</div>
);
}
// Date Range Picker Component
interface DateRangePickerProps
extends Omit<DatePickerProps, "value" | "onChange"> {
value?: { from: Date; to?: Date };
onChange?: (range: { from: Date; to?: Date } | undefined) => void;
placeholder?: string;
}
export function DateRangePicker({
value,
onChange,
placeholder = "Pick a date range",
className,
disabled = false,
showIcon = true,
minDate,
maxDate,
disabledDates,
locale = "en-US",
formatDate,
variant,
size,
...props
}: DateRangePickerProps) {
const [isOpen, setIsOpen] = React.useState(false);
const containerRef = React.useRef<HTMLDivElement>(null);
const [calendarPosition, setCalendarPosition] = React.useState({
top: 0,
left: 0,
width: 0,
});
const defaultFormatDate = (date: Date) => {
return date.toLocaleDateString(locale, {
year: "numeric",
month: "short",
day: "numeric",
});
};
const formatDateFn = formatDate || defaultFormatDate;
const handleSelect = (range: { from: Date; to?: Date }) => {
onChange?.(range);
if (range.from && range.to) {
setIsOpen(false);
}
};
const formatRange = (range: { from: Date; to?: Date }) => {
if (!range.from) return "";
if (!range.to) return formatDateFn(range.from);
return `${formatDateFn(range.from)} - ${formatDateFn(range.to)}`;
};
const handleToggleOpen = () => {
setIsOpen(!isOpen);
};
const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
handleToggleOpen();
} else if (event.key === "Escape") {
setIsOpen(false);
}
};
// Body scroll lock effect
React.useEffect(() => {
if (isOpen && typeof document !== "undefined") {
// Store original overflow
const originalOverflow = document.body.style.overflow;
// Disable scrolling
document.body.style.overflow = "hidden";
return () => {
// Restore original overflow when component unmounts or closes
document.body.style.overflow = originalOverflow;
};
}
}, [isOpen]);
// Close dropdown when clicking outside
React.useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (typeof document === "undefined") return;
const target = event.target as Node;
const isClickInsideContainer =
containerRef.current && containerRef.current.contains(target);
// Check if click is inside any calendar popup using the data attribute
const calendarElement = document.querySelector(
'[data-datepicker-calendar="true"]'
);
const isClickInsideCalendar = calendarElement?.contains(target);
if (!isClickInsideContainer && !isClickInsideCalendar) {
setIsOpen(false);
}
};
if (isOpen && typeof document !== "undefined") {
document.addEventListener("mousedown", handleClickOutside);
return () =>
document.removeEventListener("mousedown", handleClickOutside);
}
}, [isOpen]);
React.useEffect(() => {
if (typeof document !== "undefined") {
const portalRoot = document.getElementById("portal-root");
if (!portalRoot) {
const newPortalRoot = document.createElement("div");
newPortalRoot.id = "portal-root";
newPortalRoot.style.position = "relative";
newPortalRoot.style.zIndex = "9999";
document.body.appendChild(newPortalRoot);
}
}
}, []);
React.useEffect(() => {
if (isOpen && containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
setCalendarPosition({
top: rect.bottom + 8, // 8px margin
left: rect.left,
width: rect.width,
});
}
}, [isOpen]);
return (
<div className="relative" ref={containerRef} {...props}>
<button
type="button"
onClick={handleToggleOpen}
onKeyDown={handleKeyDown}
disabled={disabled}
className={cn(datePickerVariants({ variant, size }), className)}
aria-expanded={isOpen}
aria-haspopup="dialog"
aria-label="Choose date range"
>
<span className="flex items-center gap-2">
{showIcon && <CalendarIcon className="h-4 w-4 opacity-50" />}
<span
className={cn(!value && "text-[hsl(var(--hu-muted-foreground))]")}
>
{value ? formatRange(value) : placeholder}
</span>
</span>
<ChevronDown
className={cn(
"h-4 w-4 opacity-50 transition-transform duration-200",
isOpen && "rotate-180"
)}
/>
</button>
{typeof document !== "undefined" &&
ReactDOM.createPortal(
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: -10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -10, scale: 0.95 }}
transition={{ duration: 0.2 }}
className="z-[9999] rounded-[var(--radius)] border border-[hsl(var(--hu-border))] bg-[hsl(var(--hu-background))] shadow-xl"
data-datepicker-calendar="true"
style={{
position: "fixed",
top: calendarPosition.top,
left: calendarPosition.left,
width: calendarPosition.width,
}}
>
<Calendar
mode="range"
selectedRange={value}
onSelectRange={handleSelect}
minDate={minDate}
maxDate={maxDate}
disabled={disabledDates}
locale={locale}
alwaysOnTop={true}
/>
</motion.div>
)}
</AnimatePresence>,
document.getElementById("portal-root") || document.body
)}
</div>
);
}
export { datePickerVariants, type DatePickerProps, type DateRangePickerProps };
npx hextaui@latest add date-picker
pnpm dlx hextaui@latest add date-picker
yarn dlx hextaui@latest add date-picker
bun x hextaui@latest add date-picker
Usage
import { DatePicker, DateRangePicker } from "@/components/ui/DatePicker";
<DatePicker value={selectedDate} onChange={setSelectedDate} />
<DateRangePicker value={selectedRange} onChange={setSelectedRange} />
Examples
Date Range Picker
function BasicDateRangePickerExample() {
const [range, setRange] = React.useState<{ from: Date; to?: Date }>();
return (
<DateRangePicker
value={range}
onChange={setRange}
placeholder="Select date range"
/>
);
}
Variants
function DatePickerVariantsExample() {
const [date, setDate] = React.useState<Date>();
return (
<div className="space-y-4 w-full max-w-sm">
<DatePicker
value={date}
onChange={setDate}
placeholder="Default variant"
variant="default"
/>
<DatePicker
value={date}
onChange={setDate}
placeholder="Outline variant"
variant="outline"
/>
<DatePicker
value={date}
onChange={setDate}
placeholder="Ghost variant"
variant="ghost"
/>
</div>
);
}
Sizes
function DatePickerSizesExample() {
const [date, setDate] = React.useState<Date>();
return (
<div className="space-y-4 w-full max-w-sm">
<DatePicker
value={date}
onChange={setDate}
placeholder="Small size"
size="sm"
/>
<DatePicker
value={date}
onChange={setDate}
placeholder="Default size"
size="default"
/>
<DatePicker
value={date}
onChange={setDate}
placeholder="Large size"
size="lg"
/>
</div>
);
}
With Restrictions
function DatePickerWithRestrictionsExample() {
const [date, setDate] = React.useState<Date>();
const disableWeekends = (date: Date) => {
return date.getDay() === 0 || date.getDay() === 6;
};
return (
<div className="space-y-4 w-full max-w-sm">
<DatePicker
value={date}
onChange={setDate}
placeholder="Future dates only"
minDate={new Date()}
/>
<DatePicker
value={date}
onChange={setDate}
placeholder="Weekdays only"
disabledDates={disableWeekends}
/>
</div>
);
}
Custom Format
function DatePickerCustomFormatExample() {
const [date, setDate] = React.useState<Date>();
const customFormat = (date: Date) => {
return date.toLocaleDateString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
});
};
return (
<DatePicker
value={date}
onChange={setDate}
placeholder="Custom date format"
formatDate={customFormat}
/>
);
}
Form Integration
Perfect for forms with complex date requirements.
function DatePickerFormExample() {
const [formData, setFormData] = React.useState({
birthDate: undefined as Date | undefined,
vacationRange: undefined as { from: Date; to?: Date } | undefined,
});
return (
<div className="space-y-6 w-full max-w-sm">
<DatePicker
value={formData.birthDate}
onChange={(date) => setFormData(prev => ({ ...prev, birthDate: date }))}
placeholder="Select birth date"
maxDate={new Date()}
/>
<DateRangePicker
value={formData.vacationRange}
onChange={(range) => setFormData(prev => ({ ...prev, vacationRange: range }))}
placeholder="Select vacation dates"
minDate={new Date()}
/>
</div>
);
}
Booking System
Great for hotel bookings, appointment scheduling, and reservation systems.
Hotel Booking
Note:Grayed out dates are unavailable
function BookingDatePickerExample() {
const [checkIn, setCheckIn] = React.useState<Date>();
const [checkOut, setCheckOut] = React.useState<Date>();
const bookedDates = [
new Date(2025, 5, 15),
new Date(2025, 5, 16),
new Date(2025, 5, 20),
new Date(2025, 5, 25),
];
const isDateBooked = (date: Date) => {
return bookedDates.some(
(bookedDate) => date.toDateString() === bookedDate.toDateString()
);
};
const disableUnavailableDates = (date: Date) => {
const today = new Date();
today.setHours(0, 0, 0, 0);
return date < today || isDateBooked(date);
};
return (
<div className="space-y-3">
<DatePicker
value={checkIn}
onChange={setCheckIn}
placeholder="Check-in date"
disabledDates={disableUnavailableDates}
minDate={new Date()}
/>
<DatePicker
value={checkOut}
onChange={setCheckOut}
placeholder="Check-out date"
disabledDates={disableUnavailableDates}
minDate={checkIn ? new Date(checkIn.getTime() + 24 * 60 * 60 * 1000) : new Date()}
/>
</div>
);
}
Disabled State
function DatePickerDisabledExample() {
return (
<div className="space-y-4 w-full max-w-sm">
<DatePicker disabled placeholder="Disabled DatePicker" />
<DateRangePicker disabled placeholder="Disabled DateRangePicker" />
</div>
);
}
API Reference
DatePicker
Prop | Type | Default |
---|---|---|
size? | "sm" | "default" | "lg" | "default" |
variant? | "default" | "outline" | "ghost" | "default" |
formatDate? | (date: Date) => string | - |
locale? | string | "en-US" |
disabledDates? | (date: Date) => boolean | - |
maxDate? | Date | - |
minDate? | Date | - |
showIcon? | boolean | true |
disabled? | boolean | false |
placeholder? | string | "Pick a date" |
onChange? | (date: Date | undefined) => void | - |
value? | Date | undefined | - |
DateRangePicker
Prop | Type | Default |
---|---|---|
placeholder? | string | "Pick a date range" |
onChange? | (range: { from: Date; to?: Date } | undefined) => void | - |
value? | { from: Date; to?: Date } | undefined | - |
Edit on GitHub
Last updated on