Web InnoventixFreeCode

Date Input

Original · free

styled date field with a mini calendar popover

byWeb InnoventixReact + Tailwind
inpdateinputs
Open

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
inp-date.tsx
"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

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 quote

Similar components

Browse all →