Date Input
Original · freestyled date field with a mini calendar popover
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/inp-date.json"use client";
import { useEffect, useId, useRef, useState } from "react";
import type { ChangeEvent, FocusEvent, KeyboardEvent } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
/* -------------------------------------------------------------------------- */
/* Date helpers (timezone-safe, all local-midnight) */
/* -------------------------------------------------------------------------- */
const MONTHS = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const MONTHS_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const WEEKDAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
const WEEKDAYS_FULL = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
function cx(...parts: Array<string | false | null | undefined>): string {
return parts.filter(Boolean).join(" ");
}
function startOfDay(d: Date): Date {
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
}
function addDays(d: Date, n: number): Date {
return new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
}
function daysInMonth(y: number, m: number): number {
return new Date(y, m + 1, 0).getDate();
}
function addMonthsClamped(d: Date, n: number): Date {
const nd = new Date(d.getFullYear(), d.getMonth() + n, 1);
const dim = daysInMonth(nd.getFullYear(), nd.getMonth());
nd.setDate(Math.min(d.getDate(), dim));
return nd;
}
function isSameDay(a: Date | null, b: Date | null): boolean {
if (!a || !b) return false;
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
}
function clamp(d: Date, min: Date | null, max: Date | null): Date {
const t = startOfDay(d);
if (min && t.getTime() < min.getTime()) return startOfDay(min);
if (max && t.getTime() > max.getTime()) return startOfDay(max);
return t;
}
function isOutOfRange(d: Date, min: Date | null, max: Date | null): boolean {
const t = startOfDay(d).getTime();
if (min && t < min.getTime()) return true;
if (max && t > max.getTime()) return true;
return false;
}
function makeDate(y: number, mo: number, day: number): Date | null {
if (mo < 0 || mo > 11) return null;
if (day < 1 || day > daysInMonth(y, mo)) return null;
return new Date(y, mo, day);
}
function formatDate(d: Date): string {
return `${MONTHS_SHORT[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
}
function keyOf(d: Date): string {
return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
}
function parseDate(text: string): Date | null {
const t = text.trim();
if (!t) return null;
let m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(t);
if (m) return makeDate(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
m = /^(\d{1,2})[/\-.](\d{1,2})[/\-.](\d{4})$/.exec(t);
if (m) return makeDate(Number(m[3]), Number(m[1]) - 1, Number(m[2]));
m = /^([A-Za-z]{3,9})\.?\s+(\d{1,2}),?\s+(\d{4})$/.exec(t);
if (m) {
const needle = m[1].toLowerCase();
const mi = MONTHS.findIndex((name) => name.toLowerCase().startsWith(needle));
if (mi >= 0) return makeDate(Number(m[3]), mi, Number(m[2]));
}
return null;
}
/* -------------------------------------------------------------------------- */
/* Accent tokens (portable palette only) */
/* -------------------------------------------------------------------------- */
type Accent = "indigo" | "violet" | "emerald" | "rose" | "amber" | "sky";
type AccentTokens = {
selected: string;
ring: string;
focusWithin: string;
accentText: string;
todayText: string;
dot: string;
};
const ACCENTS: Record<Accent, AccentTokens> = {
indigo: {
selected: "bg-indigo-600 text-white hover:bg-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-500",
ring: "focus-visible:ring-indigo-500 dark:focus-visible:ring-indigo-400",
focusWithin:
"focus-within:border-indigo-500 focus-within:ring-2 focus-within:ring-indigo-500/25 dark:focus-within:border-indigo-400 dark:focus-within:ring-indigo-400/25",
accentText: "text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300",
todayText: "text-indigo-600 dark:text-indigo-300",
dot: "bg-indigo-500 dark:bg-indigo-400",
},
violet: {
selected: "bg-violet-600 text-white hover:bg-violet-600 dark:bg-violet-500 dark:hover:bg-violet-500",
ring: "focus-visible:ring-violet-500 dark:focus-visible:ring-violet-400",
focusWithin:
"focus-within:border-violet-500 focus-within:ring-2 focus-within:ring-violet-500/25 dark:focus-within:border-violet-400 dark:focus-within:ring-violet-400/25",
accentText: "text-violet-600 hover:text-violet-700 dark:text-violet-400 dark:hover:text-violet-300",
todayText: "text-violet-600 dark:text-violet-300",
dot: "bg-violet-500 dark:bg-violet-400",
},
emerald: {
selected: "bg-emerald-600 text-white hover:bg-emerald-600 dark:bg-emerald-500 dark:hover:bg-emerald-500",
ring: "focus-visible:ring-emerald-500 dark:focus-visible:ring-emerald-400",
focusWithin:
"focus-within:border-emerald-500 focus-within:ring-2 focus-within:ring-emerald-500/25 dark:focus-within:border-emerald-400 dark:focus-within:ring-emerald-400/25",
accentText: "text-emerald-600 hover:text-emerald-700 dark:text-emerald-400 dark:hover:text-emerald-300",
todayText: "text-emerald-600 dark:text-emerald-300",
dot: "bg-emerald-500 dark:bg-emerald-400",
},
rose: {
selected: "bg-rose-600 text-white hover:bg-rose-600 dark:bg-rose-500 dark:hover:bg-rose-500",
ring: "focus-visible:ring-rose-500 dark:focus-visible:ring-rose-400",
focusWithin:
"focus-within:border-rose-500 focus-within:ring-2 focus-within:ring-rose-500/25 dark:focus-within:border-rose-400 dark:focus-within:ring-rose-400/25",
accentText: "text-rose-600 hover:text-rose-700 dark:text-rose-400 dark:hover:text-rose-300",
todayText: "text-rose-600 dark:text-rose-300",
dot: "bg-rose-500 dark:bg-rose-400",
},
amber: {
selected: "bg-amber-500 text-slate-900 hover:bg-amber-500 dark:bg-amber-400 dark:text-slate-900 dark:hover:bg-amber-400",
ring: "focus-visible:ring-amber-500 dark:focus-visible:ring-amber-400",
focusWithin:
"focus-within:border-amber-500 focus-within:ring-2 focus-within:ring-amber-500/25 dark:focus-within:border-amber-400 dark:focus-within:ring-amber-400/25",
accentText: "text-amber-600 hover:text-amber-700 dark:text-amber-400 dark:hover:text-amber-300",
todayText: "text-amber-600 dark:text-amber-300",
dot: "bg-amber-500 dark:bg-amber-400",
},
sky: {
selected: "bg-sky-600 text-white hover:bg-sky-600 dark:bg-sky-500 dark:hover:bg-sky-500",
ring: "focus-visible:ring-sky-500 dark:focus-visible:ring-sky-400",
focusWithin:
"focus-within:border-sky-500 focus-within:ring-2 focus-within:ring-sky-500/25 dark:focus-within:border-sky-400 dark:focus-within:ring-sky-400/25",
accentText: "text-sky-600 hover:text-sky-700 dark:text-sky-400 dark:hover:text-sky-300",
todayText: "text-sky-600 dark:text-sky-300",
dot: "bg-sky-500 dark:bg-sky-400",
},
};
const DAY_BASE =
"relative flex h-9 w-9 items-center justify-center rounded-lg text-sm tabular-nums outline-none transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900";
const NAV_BTN =
"inline-flex h-8 w-8 items-center justify-center rounded-lg text-slate-500 outline-none transition-colors hover:bg-slate-100 hover:text-slate-700 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200 dark:focus-visible:ring-offset-slate-900";
/* -------------------------------------------------------------------------- */
/* Inline icons */
/* -------------------------------------------------------------------------- */
function CalendarGlyph() {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.7}
strokeLinecap="round"
strokeLinejoin="round"
className="h-[18px] w-[18px]"
aria-hidden="true"
>
<rect x="3" y="4.5" width="18" height="16" rx="2.5" />
<path d="M3 9.5h18M8 2.5v3M16 2.5v3" />
</svg>
);
}
function Chevron({ dir }: { dir: "left" | "right" | "down" }) {
const path = dir === "left" ? "M15 5l-7 7 7 7" : dir === "right" ? "M9 5l7 7-7 7" : "M6 9l6 6 6-6";
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4"
aria-hidden="true"
>
<path d={path} />
</svg>
);
}
/* -------------------------------------------------------------------------- */
/* DateField — the reusable control */
/* -------------------------------------------------------------------------- */
type DateFieldProps = {
label: string;
value: Date | null;
onChange: (d: Date | null) => void;
accent?: Accent;
min?: Date | null;
max?: Date | null;
placeholder?: string;
hint?: string;
};
function DateField({
label,
value,
onChange,
accent = "indigo",
min = null,
max = null,
placeholder = "Select a date",
hint,
}: DateFieldProps) {
const uid = useId();
const inputId = `${uid}-input`;
const hintId = `${uid}-hint`;
const dialogId = `${uid}-dialog`;
const headingId = `${uid}-heading`;
const tokens = ACCENTS[accent];
const reduce = useReducedMotion();
const today = startOfDay(new Date());
const minD = min ? startOfDay(min) : null;
const maxD = max ? startOfDay(max) : null;
const seed = value ?? today;
const [open, setOpen] = useState(false);
const [viewYear, setViewYear] = useState(seed.getFullYear());
const [viewMonth, setViewMonth] = useState(seed.getMonth());
const [focusedDate, setFocusedDate] = useState<Date>(() => clamp(value ?? today, minD, maxD));
const [inputText, setInputText] = useState<string>(() => (value ? formatDate(value) : ""));
const rootRef = useRef<HTMLDivElement | null>(null);
const gridRef = useRef<HTMLDivElement | null>(null);
const toggleRef = useRef<HTMLButtonElement | null>(null);
const dayRefs = useRef<Record<string, HTMLButtonElement | null>>({});
const pendingFocus = useRef(false);
/* keep the text field in sync when the value changes from outside typing */
useEffect(() => {
setInputText(value ? formatDate(value) : "");
}, [value]);
/* close on outside pointer */
useEffect(() => {
if (!open) return;
function onPointerDown(e: PointerEvent) {
const target = e.target as Node | null;
if (rootRef.current && target && !rootRef.current.contains(target)) setOpen(false);
}
document.addEventListener("pointerdown", onPointerDown, true);
return () => document.removeEventListener("pointerdown", onPointerDown, true);
}, [open]);
/* roving focus: move DOM focus to the focused day when requested */
useEffect(() => {
if (!open || !pendingFocus.current) return;
const el = dayRefs.current[keyOf(focusedDate)];
if (el) {
el.focus();
pendingFocus.current = false;
}
}, [open, focusedDate, viewYear, viewMonth]);
function openCalendar() {
const f = clamp(value ?? today, minD, maxD);
setFocusedDate(f);
setViewYear(f.getFullYear());
setViewMonth(f.getMonth());
pendingFocus.current = true;
setOpen(true);
}
function closeCalendar(returnFocus: boolean) {
setOpen(false);
if (returnFocus) {
requestAnimationFrame(() => toggleRef.current?.focus());
}
}
function toggle() {
if (open) closeCalendar(true);
else openCalendar();
}
function moveFocusTo(target: Date, steal: boolean) {
const next = clamp(target, minD, maxD);
setFocusedDate(next);
setViewYear(next.getFullYear());
setViewMonth(next.getMonth());
if (steal) pendingFocus.current = true;
}
function shiftMonth(delta: number) {
moveFocusTo(addMonthsClamped(focusedDate, delta), false);
}
function selectDay(cell: Date) {
if (isOutOfRange(cell, minD, maxD)) return;
onChange(startOfDay(cell));
closeCalendar(true);
}
function selectToday() {
if (isOutOfRange(today, minD, maxD)) {
moveFocusTo(today, true);
return;
}
onChange(today);
closeCalendar(true);
}
function clearValue() {
onChange(null);
closeCalendar(true);
}
function commitInput() {
const parsed = parseDate(inputText);
if (parsed && !isOutOfRange(parsed, minD, maxD)) {
const d = startOfDay(parsed);
onChange(d);
setViewYear(d.getFullYear());
setViewMonth(d.getMonth());
} else {
setInputText(value ? formatDate(value) : "");
}
}
function onInputChange(e: ChangeEvent<HTMLInputElement>) {
setInputText(e.target.value);
}
function onInputKeyDown(e: KeyboardEvent<HTMLInputElement>) {
if (e.key === "ArrowDown") {
e.preventDefault();
if (!open) openCalendar();
} else if (e.key === "Enter") {
e.preventDefault();
commitInput();
} else if (e.key === "Escape" && open) {
e.preventDefault();
closeCalendar(true);
}
}
function onInputBlur(e: FocusEvent<HTMLInputElement>) {
const next = e.relatedTarget as Node | null;
if (rootRef.current && next && rootRef.current.contains(next)) return;
commitInput();
}
function onCalendarKeyDown(e: KeyboardEvent<HTMLDivElement>) {
if (e.key === "Escape") {
e.preventDefault();
e.stopPropagation();
closeCalendar(true);
return;
}
if (!gridRef.current || !gridRef.current.contains(e.target as Node)) return;
switch (e.key) {
case "ArrowLeft":
e.preventDefault();
moveFocusTo(addDays(focusedDate, -1), true);
break;
case "ArrowRight":
e.preventDefault();
moveFocusTo(addDays(focusedDate, 1), true);
break;
case "ArrowUp":
e.preventDefault();
moveFocusTo(addDays(focusedDate, -7), true);
break;
case "ArrowDown":
e.preventDefault();
moveFocusTo(addDays(focusedDate, 7), true);
break;
case "Home":
e.preventDefault();
moveFocusTo(addDays(focusedDate, -focusedDate.getDay()), true);
break;
case "End":
e.preventDefault();
moveFocusTo(addDays(focusedDate, 6 - focusedDate.getDay()), true);
break;
case "PageUp":
e.preventDefault();
moveFocusTo(addMonthsClamped(focusedDate, e.shiftKey ? -12 : -1), true);
break;
case "PageDown":
e.preventDefault();
moveFocusTo(addMonthsClamped(focusedDate, e.shiftKey ? 12 : 1), true);
break;
default:
break;
}
}
/* build the 6x7 calendar matrix */
const first = new Date(viewYear, viewMonth, 1);
const gridStart = addDays(first, -first.getDay());
const weeks: Date[][] = [];
for (let w = 0; w < 6; w++) {
const row: Date[] = [];
for (let d = 0; d < 7; d++) row.push(addDays(gridStart, w * 7 + d));
weeks.push(row);
}
return (
<div className="relative" ref={rootRef}>
<label htmlFor={inputId} className="mb-1.5 block text-sm font-medium text-slate-700 dark:text-slate-200">
{label}
</label>
<div
className={cx(
"flex items-center gap-1.5 rounded-xl border border-slate-300 bg-white pl-2.5 transition-colors dark:border-slate-700 dark:bg-slate-900",
tokens.focusWithin
)}
>
<span className="text-slate-400 dark:text-slate-500" aria-hidden="true">
<CalendarGlyph />
</span>
<input
id={inputId}
type="text"
inputMode="text"
autoComplete="off"
spellCheck={false}
value={inputText}
placeholder={placeholder}
aria-describedby={hint ? hintId : undefined}
onChange={onInputChange}
onKeyDown={onInputKeyDown}
onBlur={onInputBlur}
className="min-w-0 flex-1 bg-transparent py-2.5 text-sm text-slate-900 outline-none placeholder:text-slate-400 dark:text-slate-100 dark:placeholder:text-slate-500"
/>
<button
ref={toggleRef}
type="button"
onClick={toggle}
aria-haspopup="dialog"
aria-expanded={open}
aria-controls={dialogId}
aria-label={open ? "Close calendar" : "Open calendar"}
className={cx(
"mr-1 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-slate-500 outline-none transition-colors hover:bg-slate-100 hover:text-slate-700 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200 dark:focus-visible:ring-offset-slate-900",
tokens.ring
)}
>
<span className={cx("transition-transform duration-200", open && "rotate-180")}>
<Chevron dir="down" />
</span>
</button>
</div>
{hint ? (
<p id={hintId} className="mt-1.5 text-xs text-slate-500 dark:text-slate-400">
{hint}
</p>
) : null}
<AnimatePresence>
{open ? (
<motion.div
key="cal"
id={dialogId}
role="dialog"
aria-label={`Choose ${label.toLowerCase()}`}
initial={reduce ? { opacity: 0 } : { opacity: 0, y: -6, scale: 0.98 }}
animate={reduce ? { opacity: 1 } : { opacity: 1, y: 0, scale: 1 }}
exit={reduce ? { opacity: 0 } : { opacity: 0, y: -6, scale: 0.98 }}
transition={{ duration: reduce ? 0 : 0.16, ease: "easeOut" }}
onKeyDown={onCalendarKeyDown}
className="absolute left-0 top-full z-50 mt-2 w-80 max-w-[calc(100vw-2.5rem)] origin-top-left rounded-2xl border border-slate-200 bg-white p-3 shadow-xl shadow-slate-900/10 dark:border-slate-700 dark:bg-slate-900 dark:shadow-black/40"
>
<div className="mb-3 flex items-center justify-between">
<button
type="button"
onClick={() => shiftMonth(-1)}
aria-label="Previous month"
className={cx(NAV_BTN, tokens.ring)}
>
<Chevron dir="left" />
</button>
<h3
id={headingId}
aria-live="polite"
className="text-sm font-semibold text-slate-800 dark:text-slate-100"
>
{MONTHS[viewMonth]} {viewYear}
</h3>
<button
type="button"
onClick={() => shiftMonth(1)}
aria-label="Next month"
className={cx(NAV_BTN, tokens.ring)}
>
<Chevron dir="right" />
</button>
</div>
<div ref={gridRef} role="grid" aria-labelledby={headingId} className="select-none">
<div role="row" className="mb-1 grid grid-cols-7">
{WEEKDAYS.map((w, i) => (
<span
key={w}
role="columnheader"
aria-label={WEEKDAYS_FULL[i]}
className="flex h-8 items-center justify-center text-[11px] font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500"
>
{w}
</span>
))}
</div>
{weeks.map((week, wi) => (
<div role="row" key={wi} className="grid grid-cols-7">
{week.map((cell) => {
const selected = isSameDay(cell, value);
const inMonth = cell.getMonth() === viewMonth;
const disabled = isOutOfRange(cell, minD, maxD);
const isToday = isSameDay(cell, today);
const isFocused = isSameDay(cell, focusedDate);
const stateClass = disabled
? "cursor-not-allowed text-slate-300 dark:text-slate-700"
: selected
? cx(tokens.selected, "font-semibold shadow-sm")
: isToday
? cx(tokens.todayText, "font-semibold hover:bg-slate-100 dark:hover:bg-slate-800")
: inMonth
? "text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-slate-800"
: "text-slate-400 hover:bg-slate-100 dark:text-slate-600 dark:hover:bg-slate-800/70";
return (
<div
role="gridcell"
aria-selected={selected}
key={keyOf(cell)}
className="flex items-center justify-center p-0.5"
>
<button
type="button"
ref={(el) => {
dayRefs.current[keyOf(cell)] = el;
}}
tabIndex={isFocused ? 0 : -1}
aria-label={`${WEEKDAYS_FULL[cell.getDay()]}, ${MONTHS[cell.getMonth()]} ${cell.getDate()}, ${cell.getFullYear()}`}
aria-current={isToday ? "date" : undefined}
aria-disabled={disabled || undefined}
onClick={() => selectDay(cell)}
className={cx(DAY_BASE, tokens.ring, stateClass)}
>
{cell.getDate()}
{isToday && !selected ? (
<span
className={cx("inpdate-animate absolute bottom-1 h-1 w-1 rounded-full", tokens.dot)}
aria-hidden="true"
/>
) : null}
</button>
</div>
);
})}
</div>
))}
</div>
<div className="mt-3 flex items-center justify-between border-t border-slate-100 pt-3 dark:border-slate-800">
<button
type="button"
onClick={selectToday}
className={cx(
"rounded-md px-1.5 py-0.5 text-xs font-semibold outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900",
tokens.accentText,
tokens.ring
)}
>
Today
</button>
<button
type="button"
onClick={clearValue}
className="rounded-md px-1.5 py-0.5 text-xs font-medium text-slate-500 outline-none transition-colors hover:text-slate-700 focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-400 dark:hover:text-slate-200 dark:focus-visible:ring-offset-slate-900"
>
Clear
</button>
</div>
</motion.div>
) : null}
</AnimatePresence>
</div>
);
}
/* -------------------------------------------------------------------------- */
/* Demo showcase (default export, no required props) */
/* -------------------------------------------------------------------------- */
export default function InpDate() {
const today = startOfDay(new Date());
const [eventDate, setEventDate] = useState<Date | null>(null);
const [startDate, setStartDate] = useState<Date | null>(today);
const [delivery, setDelivery] = useState<Date | null>(addDays(today, 7));
const [appointment, setAppointment] = useState<Date | null>(null);
const summary: Array<{ label: string; value: Date | null }> = [
{ label: "Event date", value: eventDate },
{ label: "Project start", value: startDate },
{ label: "Delivery window", value: delivery },
{ label: "Appointment", value: appointment },
];
return (
<section className="relative w-full bg-slate-50 px-4 py-16 dark:bg-slate-950 sm:px-6 sm:py-20">
<style>{`
@keyframes inpdate-pop {
0% { transform: scale(0); opacity: 0; }
60% { transform: scale(1.35); opacity: 1; }
100% { transform: scale(1); opacity: 1; }
}
@keyframes inpdate-rise {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
.inpdate-animate { animation: inpdate-pop 0.3s ease-out both; }
.inpdate-rise { animation: inpdate-rise 0.35s ease-out both; }
@media (prefers-reduced-motion: reduce) {
.inpdate-animate, .inpdate-rise { animation: none !important; }
}
`}</style>
<div className="mx-auto max-w-4xl">
<div className="mb-8">
<span className="inline-flex items-center gap-1.5 rounded-full bg-slate-100 px-3 py-1 text-xs font-medium text-slate-600 dark:bg-slate-800 dark:text-slate-300">
<span className="text-slate-500 dark:text-slate-400">
<CalendarGlyph />
</span>
Date picker
</span>
<h2 className="mt-4 text-2xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-3xl">
Schedule your project
</h2>
<p className="mt-2 max-w-prose text-sm text-slate-600 dark:text-slate-400">
Type a date or open the calendar. Arrow keys move day to day, Home and End jump across the week, Page Up
and Page Down change months, and Enter selects.
</p>
</div>
<div className="grid grid-cols-1 gap-x-6 gap-y-7 sm:grid-cols-2">
<DateField
label="Event date"
value={eventDate}
onChange={setEventDate}
accent="indigo"
placeholder="Choose a date"
hint="Pick the day your launch goes live."
/>
<DateField
label="Project start"
value={startDate}
onChange={setStartDate}
accent="sky"
placeholder="Choose a date"
hint="Defaults to today — adjust if kickoff slips."
/>
<DateField
label="Delivery window"
value={delivery}
onChange={setDelivery}
accent="emerald"
min={today}
max={addDays(today, 45)}
placeholder="Within 45 days"
hint="Must fall between today and 45 days out."
/>
<DateField
label="Appointment"
value={appointment}
onChange={setAppointment}
accent="violet"
min={today}
placeholder="No date yet"
hint="Any day from today onward."
/>
</div>
<div className="inpdate-rise mt-10 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<h3 className="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
Selected schedule
</h3>
<dl className="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2">
{summary.map((row) => (
<div
key={row.label}
className="flex items-center justify-between rounded-lg bg-slate-50 px-3 py-2 dark:bg-slate-800/60"
>
<dt className="text-sm text-slate-500 dark:text-slate-400">{row.label}</dt>
<dd className="text-sm font-medium tabular-nums text-slate-900 dark:text-slate-100">
{row.value ? formatDate(row.value) : "—"}
</dd>
</div>
))}
</dl>
</div>
</div>
</section>
);
}Dependencies
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 →
Basic Set Input
Originaldefault, hover, focus, disabled, error text inputs

Floating Label Input
Originallabel that floats up on focus/fill

Filled Input
Originalfilled material-style inputs

Underline Input
Originalminimal underline inputs with animated bar

Search Input
Originalsearch box with icon and clear

Search Suggest Input
Originalsearch with a live suggestions dropdown

Password Toggle Input
Originalpassword field with show/hide toggle

Textarea Autosize Input
Originaltextarea that grows with content

Addon Group Input
Originalinput group with leading/trailing addons

Currency Input
Originalcurrency input with formatting

Tags Input
Originaltag input, add and remove chips

OTP Input
Original6-box one-time-code input with paste

