Calendar Datetime
Original · freecombined date and time picker
byWeb InnoventixReact + Tailwind
calendardatetimecalendars
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/calendar-datetime.jsoncalendar-datetime.tsx
"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { KeyboardEvent as ReactKeyboardEvent } from "react";
import { motion, useReducedMotion } from "motion/react";
/* ---------- date helpers (no external libs) ---------- */
const WEEKDAY_SHORT = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const;
const WEEKDAY_LONG = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
] as const;
function startOfDay(d: Date): Date {
const x = new Date(d);
x.setHours(0, 0, 0, 0);
return x;
}
function addDays(d: Date, n: number): Date {
const x = new Date(d);
x.setDate(x.getDate() + n);
return x;
}
function addMonths(d: Date, n: number): Date {
const target = new Date(d.getFullYear(), d.getMonth() + n, 1);
const dim = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
target.setDate(Math.min(d.getDate(), dim));
target.setHours(0, 0, 0, 0);
return target;
}
function isSameDay(a: Date, b: Date): boolean {
return (
a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate()
);
}
function keyFor(d: Date): string {
return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
}
const dateFmt = new Intl.DateTimeFormat("en-US", {
weekday: "long",
month: "long",
day: "numeric",
year: "numeric",
});
const monthFmt = new Intl.DateTimeFormat("en-US", { month: "long", year: "numeric" });
const timeFmt = new Intl.DateTimeFormat("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
/* ---------- inline icons ---------- */
type IconProps = { className?: string };
function ChevronLeft({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="m15 18-6-6 6-6" />
</svg>
);
}
function ChevronRight({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="m9 18 6-6-6-6" />
</svg>
);
}
function ChevronUp({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="m18 15-6-6-6 6" />
</svg>
);
}
function ChevronDown({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="m6 9 6 6 6-6" />
</svg>
);
}
function CalendarIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<rect x="3" y="4" width="18" height="18" rx="2" />
<path d="M16 2v4M8 2v4M3 10h18" />
</svg>
);
}
function ClockIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<circle cx="12" cy="12" r="9" />
<path d="M12 7v5l3 2" />
</svg>
);
}
function CheckIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="M20 6 9 17l-5-5" />
</svg>
);
}
/* ---------- component ---------- */
export default function CalendarDatetime() {
const reduce = useReducedMotion();
// Fixed reference "today" keeps the demo deterministic (no SSR/hydration drift).
const today = useMemo(() => startOfDay(new Date(2026, 6, 16)), []);
const [selected, setSelected] = useState<Date>(() => new Date(2026, 6, 16, 14, 30, 0, 0));
const [view, setView] = useState<{ year: number; month: number }>(() => ({
year: 2026,
month: 6,
}));
const [focusedDay, setFocusedDay] = useState<Date>(() => new Date(2026, 6, 16));
const [direction, setDirection] = useState<number>(0);
const [confirmed, setConfirmed] = useState<boolean>(false);
const [tzLabel, setTzLabel] = useState<string>("");
const gridRef = useRef<HTMLDivElement | null>(null);
const shouldFocusRef = useRef<boolean>(false);
const amRef = useRef<HTMLButtonElement | null>(null);
const pmRef = useRef<HTMLButtonElement | null>(null);
useEffect(() => {
try {
const parts = new Intl.DateTimeFormat(undefined, {
timeZoneName: "short",
hour: "2-digit",
}).formatToParts(new Date());
const zone = parts.find((p) => p.type === "timeZoneName");
if (zone) setTzLabel(zone.value);
} catch {
/* ignore */
}
}, []);
/* build a fixed 6x7 grid for the visible month */
const weeks = useMemo(() => {
const first = new Date(view.year, view.month, 1);
const gridStart = addDays(first, -first.getDay());
const rows: Date[][] = [];
for (let r = 0; r < 6; r++) {
const row: Date[] = [];
for (let c = 0; c < 7; c++) row.push(addDays(gridStart, r * 7 + c));
rows.push(row);
}
return rows;
}, [view]);
/* keep the roving-tabindex target inside the visible month */
useEffect(() => {
setFocusedDay((fd) => {
if (fd.getFullYear() === view.year && fd.getMonth() === view.month) return fd;
const dim = new Date(view.year, view.month + 1, 0).getDate();
return new Date(view.year, view.month, Math.min(fd.getDate(), dim));
});
}, [view]);
/* move DOM focus to the roving day when a keyboard action asked for it */
useEffect(() => {
if (!shouldFocusRef.current) return;
const el = gridRef.current?.querySelector<HTMLButtonElement>(
`[data-day="${keyFor(focusedDay)}"]`,
);
if (el) {
el.focus();
shouldFocusRef.current = false;
}
}, [focusedDay, view]);
const moveFocus = useCallback(
(next: Date) => {
shouldFocusRef.current = true;
setFocusedDay(next);
if (next.getMonth() !== view.month || next.getFullYear() !== view.year) {
setDirection(next.getTime() > focusedDay.getTime() ? 1 : -1);
setView({ year: next.getFullYear(), month: next.getMonth() });
}
},
[view, focusedDay],
);
const shiftMonth = useCallback((delta: number) => {
setDirection(delta);
setView((v) => {
const m = v.month + delta;
return {
year: v.year + Math.floor(m / 12),
month: ((m % 12) + 12) % 12,
};
});
}, []);
const selectDay = useCallback((day: Date) => {
setSelected((prev) => {
const next = new Date(day);
next.setHours(prev.getHours(), prev.getMinutes(), 0, 0);
return next;
});
setFocusedDay(day);
setConfirmed(false);
}, []);
const goToday = useCallback(() => {
setSelected((prev) => {
const next = new Date(today);
next.setHours(prev.getHours(), prev.getMinutes(), 0, 0);
return next;
});
setDirection(0);
setView({ year: today.getFullYear(), month: today.getMonth() });
setFocusedDay(new Date(today));
shouldFocusRef.current = true;
setConfirmed(false);
}, [today]);
const onDayKeyDown = useCallback(
(e: ReactKeyboardEvent<HTMLButtonElement>, day: Date) => {
switch (e.key) {
case "ArrowLeft":
e.preventDefault();
moveFocus(addDays(day, -1));
break;
case "ArrowRight":
e.preventDefault();
moveFocus(addDays(day, 1));
break;
case "ArrowUp":
e.preventDefault();
moveFocus(addDays(day, -7));
break;
case "ArrowDown":
e.preventDefault();
moveFocus(addDays(day, 7));
break;
case "Home":
e.preventDefault();
moveFocus(addDays(day, -day.getDay()));
break;
case "End":
e.preventDefault();
moveFocus(addDays(day, 6 - day.getDay()));
break;
case "PageUp":
e.preventDefault();
moveFocus(addMonths(day, -1));
break;
case "PageDown":
e.preventDefault();
moveFocus(addMonths(day, 1));
break;
case "Enter":
case " ":
e.preventDefault();
selectDay(day);
break;
default:
break;
}
},
[moveFocus, selectDay],
);
/* ---- time ---- */
const hours24 = selected.getHours();
const minutes = selected.getMinutes();
const meridiem: "AM" | "PM" = hours24 < 12 ? "AM" : "PM";
const displayHour = ((hours24 + 11) % 12) + 1;
const updateTime = useCallback((h24: number, m: number) => {
setSelected((prev) => {
const next = new Date(prev);
next.setHours(h24, m, 0, 0);
return next;
});
setConfirmed(false);
}, []);
const setHour12 = useCallback(
(dh: number) => {
const h24 = (dh % 12) + (meridiem === "PM" ? 12 : 0);
updateTime(h24, minutes);
},
[meridiem, minutes, updateTime],
);
const stepHour = useCallback(
(delta: number) => {
setHour12(((displayHour - 1 + delta + 12) % 12) + 1);
},
[displayHour, setHour12],
);
const stepMinute = useCallback(
(delta: number) => {
updateTime(hours24, (minutes + delta + 60) % 60);
},
[hours24, minutes, updateTime],
);
const setMeridiemVal = useCallback(
(mer: "AM" | "PM") => {
if (mer === meridiem) return;
const h24 = mer === "PM" ? (hours24 % 12) + 12 : hours24 % 12;
updateTime(h24, minutes);
},
[meridiem, hours24, minutes, updateTime],
);
const onHourKey = useCallback(
(e: ReactKeyboardEvent<HTMLDivElement>) => {
switch (e.key) {
case "ArrowUp":
case "ArrowRight":
case "PageUp":
e.preventDefault();
stepHour(1);
break;
case "ArrowDown":
case "ArrowLeft":
case "PageDown":
e.preventDefault();
stepHour(-1);
break;
case "Home":
e.preventDefault();
setHour12(12);
break;
case "End":
e.preventDefault();
setHour12(11);
break;
default:
break;
}
},
[stepHour, setHour12],
);
const onMinuteKey = useCallback(
(e: ReactKeyboardEvent<HTMLDivElement>) => {
switch (e.key) {
case "ArrowUp":
case "ArrowRight":
e.preventDefault();
stepMinute(1);
break;
case "ArrowDown":
case "ArrowLeft":
e.preventDefault();
stepMinute(-1);
break;
case "PageUp":
e.preventDefault();
stepMinute(5);
break;
case "PageDown":
e.preventDefault();
stepMinute(-5);
break;
case "Home":
e.preventDefault();
updateTime(hours24, 0);
break;
case "End":
e.preventDefault();
updateTime(hours24, 59);
break;
default:
break;
}
},
[stepMinute, updateTime, hours24],
);
const onMeridiemKey = useCallback(
(e: ReactKeyboardEvent<HTMLButtonElement>) => {
if (
e.key === "ArrowUp" ||
e.key === "ArrowDown" ||
e.key === "ArrowLeft" ||
e.key === "ArrowRight"
) {
e.preventDefault();
const next = meridiem === "AM" ? "PM" : "AM";
setMeridiemVal(next);
(next === "AM" ? amRef : pmRef).current?.focus();
}
},
[meridiem, setMeridiemVal],
);
const timePresets: { label: string; h: number; m: number }[] = [
{ label: "9:00 AM", h: 9, m: 0 },
{ label: "12:30 PM", h: 12, m: 30 },
{ label: "3:15 PM", h: 15, m: 15 },
{ label: "5:45 PM", h: 17, m: 45 },
];
const monthVariants = reduce
? {
enter: { opacity: 0 },
center: { opacity: 1 },
}
: {
enter: (dir: number) => ({ opacity: 0, x: dir >= 0 ? 22 : -22 }),
center: { opacity: 1, x: 0 },
};
const summary = `${dateFmt.format(selected)} at ${timeFmt.format(selected)}`;
return (
<section className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 via-slate-50 to-slate-100 px-4 py-16 sm:px-6 sm:py-20 lg:py-24 dark:from-zinc-950 dark:via-zinc-950 dark:to-black">
<style>{`
@keyframes cdt-rise {
0% { opacity: 0; transform: translateY(14px); }
100% { opacity: 1; transform: translateY(0); }
}
@keyframes cdt-pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.55; transform: scale(1.35); }
}
@keyframes cdt-check {
0% { opacity: 0; transform: scale(0.4) rotate(-12deg); }
60% { transform: scale(1.15) rotate(0deg); }
100% { opacity: 1; transform: scale(1) rotate(0deg); }
}
.cdt-rise { animation: cdt-rise 0.55s cubic-bezier(0.22, 1, 0.36, 1) both; }
.cdt-pulse { animation: cdt-pulse 2.4s ease-in-out infinite; }
.cdt-check { animation: cdt-check 0.35s cubic-bezier(0.34, 1.56, 0.64, 1) both; }
@media (prefers-reduced-motion: reduce) {
.cdt-rise, .cdt-pulse, .cdt-check { animation: none !important; }
}
`}</style>
{/* soft ambient blobs */}
<div aria-hidden="true" className="pointer-events-none absolute -left-24 top-0 h-72 w-72 rounded-full bg-indigo-300/20 blur-3xl dark:bg-indigo-600/10" />
<div aria-hidden="true" className="pointer-events-none absolute -right-20 bottom-0 h-72 w-72 rounded-full bg-violet-300/20 blur-3xl dark:bg-violet-600/10" />
<div className="relative mx-auto w-full max-w-3xl">
<header className="mb-8 flex flex-col items-start gap-3">
<span className="inline-flex items-center gap-1.5 rounded-full border border-indigo-200 bg-indigo-50 px-3 py-1 text-xs font-medium text-indigo-700 dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-300">
<CalendarIcon className="h-3.5 w-3.5" />
Scheduling
</span>
<h2 className="text-2xl font-semibold tracking-tight text-slate-900 sm:text-3xl dark:text-white">
Pick a date & time
</h2>
<p className="max-w-md text-sm text-slate-500 dark:text-zinc-400">
Choose when the design review with the Northwind team should happen. Use the
arrow keys inside the calendar and the time steppers.
</p>
</header>
<div className="cdt-rise overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-xl shadow-slate-900/5 dark:border-zinc-800 dark:bg-zinc-900 dark:shadow-black/40">
<div className="h-1 w-full bg-gradient-to-r from-indigo-500 via-violet-500 to-sky-500" />
<div className="flex flex-col md:flex-row">
{/* ---------------- calendar ---------------- */}
<div className="p-5 sm:p-6 md:flex-1">
<div className="mb-4 flex items-center justify-between">
<button
type="button"
onClick={() => shiftMonth(-1)}
aria-label="Previous month"
className="flex h-9 w-9 items-center justify-center rounded-xl text-slate-500 outline-none transition-colors hover:bg-slate-100 hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-white dark:focus-visible:ring-offset-zinc-900"
>
<ChevronLeft className="h-5 w-5" />
</button>
<div aria-live="polite" className="text-sm font-semibold text-slate-900 tabular-nums dark:text-white">
{monthFmt.format(new Date(view.year, view.month, 1))}
</div>
<button
type="button"
onClick={() => shiftMonth(1)}
aria-label="Next month"
className="flex h-9 w-9 items-center justify-center rounded-xl text-slate-500 outline-none transition-colors hover:bg-slate-100 hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-white dark:focus-visible:ring-offset-zinc-900"
>
<ChevronRight className="h-5 w-5" />
</button>
</div>
<div ref={gridRef} role="grid" aria-label="Choose a date" className="select-none">
<div role="row" className="grid grid-cols-7 gap-1">
{WEEKDAY_SHORT.map((w, i) => (
<span
key={w}
role="columnheader"
aria-label={WEEKDAY_LONG[i]}
className="flex h-8 items-center justify-center text-xs font-medium text-slate-400 dark:text-zinc-500"
>
{w}
</span>
))}
</div>
<motion.div
key={`${view.year}-${view.month}`}
role="rowgroup"
custom={direction}
variants={monthVariants}
initial="enter"
animate="center"
transition={{ duration: reduce ? 0 : 0.26, ease: "easeOut" }}
className="mt-1 space-y-1"
>
{weeks.map((week, wi) => (
<div key={wi} role="row" className="grid grid-cols-7 gap-1">
{week.map((day) => {
const outside = day.getMonth() !== view.month;
const isSelected = isSameDay(day, selected);
const isToday = isSameDay(day, today);
const isRoving = isSameDay(day, focusedDay);
let tone =
"text-slate-700 hover:bg-slate-100 dark:text-zinc-200 dark:hover:bg-zinc-800";
if (isSelected) {
tone =
"bg-indigo-600 text-white shadow-sm shadow-indigo-600/40 hover:bg-indigo-600";
} else if (outside) {
tone =
"text-slate-300 hover:bg-slate-100 dark:text-zinc-600 dark:hover:bg-zinc-800/60";
}
const ring =
isToday && !isSelected
? " ring-1 ring-inset ring-indigo-400/60 dark:ring-indigo-400/50"
: "";
return (
<div key={keyFor(day)} role="gridcell" aria-selected={isSelected}>
<button
type="button"
data-day={keyFor(day)}
tabIndex={isRoving ? 0 : -1}
onClick={() => selectDay(day)}
onKeyDown={(e) => onDayKeyDown(e, day)}
aria-label={dateFmt.format(day)}
aria-current={isToday ? "date" : undefined}
className={`relative flex h-10 w-full items-center justify-center rounded-xl text-sm font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-zinc-900 ${tone}${ring}`}
>
{day.getDate()}
{isToday && (
<span
aria-hidden="true"
className={`cdt-pulse absolute bottom-1.5 h-1 w-1 rounded-full ${
isSelected ? "bg-white" : "bg-indigo-500 dark:bg-indigo-400"
}`}
/>
)}
</button>
</div>
);
})}
</div>
))}
</motion.div>
</div>
<div className="mt-4 flex justify-start">
<button
type="button"
onClick={goToday}
className="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium text-indigo-600 outline-none transition-colors hover:bg-indigo-50 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-indigo-400 dark:hover:bg-indigo-500/10 dark:focus-visible:ring-offset-zinc-900"
>
<CalendarIcon className="h-3.5 w-3.5" />
Jump to today
</button>
</div>
</div>
{/* ---------------- time ---------------- */}
<div className="border-t border-slate-200 p-5 sm:p-6 md:w-64 md:border-l md:border-t-0 dark:border-zinc-800">
<div className="mb-4 flex items-center justify-between">
<h3 className="inline-flex items-center gap-1.5 text-sm font-semibold text-slate-900 dark:text-white">
<ClockIcon className="h-4 w-4 text-slate-400 dark:text-zinc-500" />
Time
</h3>
</div>
<div className="flex items-stretch justify-center gap-2">
{/* hour */}
<div className="flex flex-col items-center gap-1">
<button
type="button"
tabIndex={-1}
aria-hidden="true"
onClick={() => stepHour(1)}
className="flex h-7 w-12 items-center justify-center rounded-lg text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700 dark:text-zinc-500 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
>
<ChevronUp className="h-4 w-4" />
</button>
<div
role="spinbutton"
tabIndex={0}
aria-label="Hour"
aria-valuemin={1}
aria-valuemax={12}
aria-valuenow={displayHour}
aria-valuetext={`${displayHour} ${meridiem}`}
onKeyDown={onHourKey}
className="flex h-14 w-12 items-center justify-center rounded-xl border border-slate-200 bg-slate-50 text-2xl font-semibold tabular-nums text-slate-900 outline-none transition-colors focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-zinc-700 dark:bg-zinc-800 dark:text-white dark:focus-visible:ring-offset-zinc-900"
>
{displayHour}
</div>
<button
type="button"
tabIndex={-1}
aria-hidden="true"
onClick={() => stepHour(-1)}
className="flex h-7 w-12 items-center justify-center rounded-lg text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700 dark:text-zinc-500 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
>
<ChevronDown className="h-4 w-4" />
</button>
</div>
<span aria-hidden="true" className="flex items-center pb-0 text-2xl font-semibold text-slate-300 dark:text-zinc-600">
:
</span>
{/* minute */}
<div className="flex flex-col items-center gap-1">
<button
type="button"
tabIndex={-1}
aria-hidden="true"
onClick={() => stepMinute(1)}
className="flex h-7 w-12 items-center justify-center rounded-lg text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700 dark:text-zinc-500 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
>
<ChevronUp className="h-4 w-4" />
</button>
<div
role="spinbutton"
tabIndex={0}
aria-label="Minute"
aria-valuemin={0}
aria-valuemax={59}
aria-valuenow={minutes}
aria-valuetext={`${String(minutes).padStart(2, "0")} minutes`}
onKeyDown={onMinuteKey}
className="flex h-14 w-12 items-center justify-center rounded-xl border border-slate-200 bg-slate-50 text-2xl font-semibold tabular-nums text-slate-900 outline-none transition-colors focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-zinc-700 dark:bg-zinc-800 dark:text-white dark:focus-visible:ring-offset-zinc-900"
>
{String(minutes).padStart(2, "0")}
</div>
<button
type="button"
tabIndex={-1}
aria-hidden="true"
onClick={() => stepMinute(-1)}
className="flex h-7 w-12 items-center justify-center rounded-lg text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700 dark:text-zinc-500 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
>
<ChevronDown className="h-4 w-4" />
</button>
</div>
{/* meridiem */}
<div role="radiogroup" aria-label="AM or PM" className="flex flex-col justify-center gap-1 pl-1">
{(["AM", "PM"] as const).map((m) => {
const checked = meridiem === m;
return (
<button
key={m}
ref={m === "AM" ? amRef : pmRef}
type="button"
role="radio"
aria-checked={checked}
tabIndex={checked ? 0 : -1}
onClick={() => setMeridiemVal(m)}
onKeyDown={onMeridiemKey}
className={`rounded-lg px-3 py-1.5 text-xs font-semibold outline-none transition-colors focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-zinc-900 ${
checked
? "bg-indigo-600 text-white shadow-sm shadow-indigo-600/40"
: "border border-slate-200 text-slate-600 hover:bg-slate-100 dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800"
}`}
>
{m}
</button>
);
})}
</div>
</div>
<div className="mt-6">
<p className="mb-2 text-xs font-medium text-slate-400 dark:text-zinc-500">
Quick times
</p>
<div className="grid grid-cols-2 gap-2">
{timePresets.map((p) => {
const active = hours24 === p.h && minutes === p.m;
return (
<button
key={p.label}
type="button"
onClick={() => updateTime(p.h, p.m)}
aria-pressed={active}
className={`rounded-xl border px-2 py-2 text-xs font-medium tabular-nums outline-none transition-colors focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-zinc-900 ${
active
? "border-indigo-500 bg-indigo-50 text-indigo-700 dark:border-indigo-500/60 dark:bg-indigo-500/10 dark:text-indigo-300"
: "border-slate-200 text-slate-600 hover:border-slate-300 hover:bg-slate-50 dark:border-zinc-700 dark:text-zinc-300 dark:hover:border-zinc-600 dark:hover:bg-zinc-800"
}`}
>
{p.label}
</button>
);
})}
</div>
</div>
</div>
</div>
{/* ---------------- footer summary ---------------- */}
<div className="flex flex-col gap-3 border-t border-slate-200 bg-slate-50 px-5 py-4 sm:flex-row sm:items-center sm:justify-between sm:px-6 dark:border-zinc-800 dark:bg-zinc-950/60">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 flex-none items-center justify-center rounded-xl bg-white text-indigo-600 shadow-sm ring-1 ring-slate-200 dark:bg-zinc-900 dark:text-indigo-400 dark:ring-zinc-800">
<CalendarIcon className="h-5 w-5" />
</div>
<div className="min-w-0">
<div className="truncate text-sm font-semibold text-slate-900 dark:text-white">
{dateFmt.format(selected)}
</div>
<div className="text-xs text-slate-500 tabular-nums dark:text-zinc-400">
{timeFmt.format(selected)}
{tzLabel ? ` · ${tzLabel}` : ""}
</div>
</div>
</div>
<button
type="button"
onClick={() => setConfirmed(true)}
className={`inline-flex flex-none items-center justify-center gap-2 rounded-xl px-5 py-2.5 text-sm font-semibold outline-none transition-colors focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:focus-visible:ring-offset-zinc-950 ${
confirmed
? "bg-emerald-600 text-white shadow-sm shadow-emerald-600/30 hover:bg-emerald-600"
: "bg-indigo-600 text-white shadow-sm shadow-indigo-600/30 hover:bg-indigo-500"
}`}
>
{confirmed ? (
<>
<CheckIcon className="cdt-check h-4 w-4" />
Confirmed
</>
) : (
"Confirm time"
)}
</button>
</div>
</div>
<p className="sr-only" role="status" aria-live="polite">
{confirmed ? `Confirmed for ${summary}` : `Selected ${summary}`}
</p>
</div>
</section>
);
}Dependencies
motion
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Calendar Date Picker
Originaldate picker with a dropdown calendar

Calendar Range Picker
Originaldate range picker with two months

Calendar Inline
Originalalways-visible inline calendar

Calendar Month Year
Originalmonth and year quick selector

Calendar Event
Originalmonth calendar with event dots

Calendar Mini
Originalcompact mini calendar widget

Calendar Time Picker
Originaltime picker with hour/minute

Calendar Week
Originalweek view with time slots

Calendar Agenda
Originalagenda list of upcoming days

Calendar Availability
Originalavailability slot picker

Calendar Booking
Originalbooking calendar with time slots

Calendar Multi Month
Originaltwo-month scrolling calendar

