Calendar Event
Original · freemonth calendar with event dots
byWeb InnoventixReact + Tailwind
calendareventcalendars
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-event.jsoncalendar-event.tsx
"use client";
import {
useEffect,
useId,
useMemo,
useRef,
useState,
type FormEvent,
type KeyboardEvent,
} from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type CategoryKey = "work" | "personal" | "health" | "social" | "travel";
type EventItem = {
id: string;
title: string;
time: string; // "HH:MM" 24h
category: CategoryKey;
};
type CategoryMeta = {
key: CategoryKey;
label: string;
dot: string;
badge: string;
};
const CATEGORIES: readonly CategoryMeta[] = [
{
key: "work",
label: "Work",
dot: "bg-indigo-500",
badge:
"bg-indigo-50 text-indigo-700 ring-indigo-200 dark:bg-indigo-400/10 dark:text-indigo-300 dark:ring-indigo-400/30",
},
{
key: "personal",
label: "Personal",
dot: "bg-emerald-500",
badge:
"bg-emerald-50 text-emerald-700 ring-emerald-200 dark:bg-emerald-400/10 dark:text-emerald-300 dark:ring-emerald-400/30",
},
{
key: "health",
label: "Health",
dot: "bg-rose-500",
badge:
"bg-rose-50 text-rose-700 ring-rose-200 dark:bg-rose-400/10 dark:text-rose-300 dark:ring-rose-400/30",
},
{
key: "social",
label: "Social",
dot: "bg-amber-500",
badge:
"bg-amber-50 text-amber-700 ring-amber-200 dark:bg-amber-400/10 dark:text-amber-300 dark:ring-amber-400/30",
},
{
key: "travel",
label: "Travel",
dot: "bg-sky-500",
badge:
"bg-sky-50 text-sky-700 ring-sky-200 dark:bg-sky-400/10 dark:text-sky-300 dark:ring-sky-400/30",
},
] as const;
const META_BY_KEY: Record<CategoryKey, CategoryMeta> = CATEGORIES.reduce(
(acc, c) => {
acc[c.key] = c;
return acc;
},
{} as Record<CategoryKey, CategoryMeta>,
);
const WEEKDAYS = [
{ short: "Sun", long: "Sunday" },
{ short: "Mon", long: "Monday" },
{ short: "Tue", long: "Tuesday" },
{ short: "Wed", long: "Wednesday" },
{ short: "Thu", long: "Thursday" },
{ short: "Fri", long: "Friday" },
{ short: "Sat", long: "Saturday" },
] as const;
const MONTHS = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
] as const;
const TODAY_ISO = "2026-07-16";
const pad = (n: number): string => String(n).padStart(2, "0");
const isoOf = (d: Date): string =>
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const parseISO = (s: string): Date => {
const [y, m, d] = s.split("-").map(Number);
return new Date(y, m - 1, d);
};
const daysInMonth = (y: number, m: number): number =>
new Date(y, m + 1, 0).getDate();
const formatFull = (d: Date): string =>
`${WEEKDAYS[d.getDay()].long}, ${MONTHS[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
const formatTime = (t: string): string => {
const [h, m] = t.split(":").map(Number);
const ap = h >= 12 ? "PM" : "AM";
const hr = h % 12 === 0 ? 12 : h % 12;
return `${hr}:${pad(m)} ${ap}`;
};
function buildMonth(year: number, month: number): Date[][] {
const first = new Date(year, month, 1);
const cursor = new Date(year, month, 1 - first.getDay());
const weeks: Date[][] = [];
for (let w = 0; w < 6; w++) {
const days: Date[] = [];
for (let d = 0; d < 7; d++) {
days.push(new Date(cursor));
cursor.setDate(cursor.getDate() + 1);
}
weeks.push(days);
}
return weeks;
}
function seedEvents(): Record<string, EventItem[]> {
const raw: Record<string, Omit<EventItem, "id">[]> = {
"2026-06-30": [{ title: "Q2 invoices due", time: "17:00", category: "work" }],
"2026-07-02": [
{ title: "Design sync with Priya", time: "10:00", category: "work" },
],
"2026-07-03": [
{ title: "Dentist — Dr. Okafor", time: "15:30", category: "health" },
],
"2026-07-04": [
{ title: "Rooftop BBQ at Marco's", time: "18:00", category: "social" },
],
"2026-07-06": [
{ title: "Q3 roadmap review", time: "09:00", category: "work" },
{ title: "Vinyasa yoga", time: "19:00", category: "health" },
],
"2026-07-09": [
{ title: "Flight to Lisbon · TP204", time: "13:15", category: "travel" },
],
"2026-07-10": [
{ title: "Belém pastéis tour", time: "11:00", category: "travel" },
],
"2026-07-13": [
{ title: "Alfama sunset walk", time: "19:30", category: "travel" },
],
"2026-07-14": [
{ title: "Return flight · TP205", time: "08:40", category: "travel" },
],
"2026-07-16": [
{ title: "1:1 with Salman", time: "11:00", category: "work" },
{ title: "Grocery run", time: "18:30", category: "personal" },
],
"2026-07-17": [
{ title: "Mom's birthday dinner", time: "19:30", category: "personal" },
],
"2026-07-20": [
{ title: "Sprint planning", time: "09:30", category: "work" },
],
"2026-07-22": [
{ title: "5K morning run", time: "06:30", category: "health" },
],
"2026-07-24": [
{ title: "Ship Web Innoventix v2", time: "16:00", category: "work" },
{ title: "Team happy hour", time: "17:30", category: "social" },
],
"2026-07-25": [
{ title: "Farmers market", time: "09:00", category: "personal" },
],
"2026-07-27": [
{ title: "Client demo — Acme Corp", time: "14:00", category: "work" },
],
"2026-07-29": [
{ title: "Vinyasa yoga", time: "19:00", category: "health" },
],
"2026-07-31": [
{ title: "Month-end review", time: "16:00", category: "work" },
{ title: "Movie night · Dune 3", time: "20:00", category: "social" },
],
"2026-08-01": [
{ title: "Camping — Big Sur", time: "08:00", category: "travel" },
],
};
const out: Record<string, EventItem[]> = {};
for (const iso of Object.keys(raw)) {
out[iso] = raw[iso].map((e, i) => ({ ...e, id: `${iso}#s${i}` }));
}
return out;
}
export default function CalendarEvent() {
const reduce = useReducedMotion();
const uid = useId();
const headingId = `${uid}-heading`;
const panelId = `${uid}-panel`;
const today = useMemo(() => parseISO(TODAY_ISO), []);
const [view, setView] = useState<{ y: number; m: number }>({
y: today.getFullYear(),
m: today.getMonth(),
});
const [selected, setSelected] = useState<string>(TODAY_ISO);
const [focused, setFocused] = useState<string>(TODAY_ISO);
const [events, setEvents] = useState<Record<string, EventItem[]>>(seedEvents);
const [activeCats, setActiveCats] = useState<Set<CategoryKey>>(
() => new Set(CATEGORIES.map((c) => c.key)),
);
const [showForm, setShowForm] = useState(false);
const [formTitle, setFormTitle] = useState("");
const [formTime, setFormTime] = useState("09:00");
const [formCat, setFormCat] = useState<CategoryKey>("work");
const dayRefs = useRef<Record<string, HTMLButtonElement | null>>({});
const shouldFocusRef = useRef(false);
const idRef = useRef(0);
useEffect(() => {
if (shouldFocusRef.current) {
const el = dayRefs.current[focused];
if (el) el.focus();
shouldFocusRef.current = false;
}
}, [focused, view]);
const weeks = useMemo(() => buildMonth(view.y, view.m), [view]);
const visibleOf = useMemo(
() =>
(iso: string): EventItem[] => {
const list = events[iso];
if (!list) return [];
return list
.filter((e) => activeCats.has(e.category))
.slice()
.sort((a, b) => a.time.localeCompare(b.time));
},
[events, activeCats],
);
const monthCount = useMemo(() => {
let c = 0;
for (const iso of Object.keys(events)) {
const d = parseISO(iso);
if (d.getFullYear() === view.y && d.getMonth() === view.m) {
c += visibleOf(iso).length;
}
}
return c;
}, [events, view, visibleOf]);
const goMonth = (delta: number) => {
const base = new Date(view.y, view.m + delta, 1);
const ny = base.getFullYear();
const nm = base.getMonth();
const fd = parseISO(focused);
const day = Math.min(fd.getDate(), daysInMonth(ny, nm));
setView({ y: ny, m: nm });
setFocused(isoOf(new Date(ny, nm, day)));
};
const goToday = () => {
setView({ y: today.getFullYear(), m: today.getMonth() });
setSelected(TODAY_ISO);
setFocused(TODAY_ISO);
shouldFocusRef.current = true;
};
const pickDate = (d: Date) => {
const iso = isoOf(d);
setView({ y: d.getFullYear(), m: d.getMonth() });
setSelected(iso);
setFocused(iso);
shouldFocusRef.current = true;
};
const moveFocus = (d: Date) => {
setView({ y: d.getFullYear(), m: d.getMonth() });
setFocused(isoOf(d));
shouldFocusRef.current = true;
};
const onDayKeyDown = (e: KeyboardEvent<HTMLButtonElement>, iso: string) => {
const d = parseISO(iso);
const y = d.getFullYear();
const m = d.getMonth();
const day = d.getDate();
let next: Date | null = null;
switch (e.key) {
case "ArrowLeft":
next = new Date(y, m, day - 1);
break;
case "ArrowRight":
next = new Date(y, m, day + 1);
break;
case "ArrowUp":
next = new Date(y, m, day - 7);
break;
case "ArrowDown":
next = new Date(y, m, day + 7);
break;
case "Home":
next = new Date(y, m, day - d.getDay());
break;
case "End":
next = new Date(y, m, day + (6 - d.getDay()));
break;
case "PageUp":
next = new Date(y, m - 1, day);
break;
case "PageDown":
next = new Date(y, m + 1, day);
break;
case "Enter":
case " ":
e.preventDefault();
setSelected(iso);
return;
default:
return;
}
e.preventDefault();
if (next) moveFocus(next);
};
const toggleCat = (key: CategoryKey) => {
setActiveCats((prev) => {
const nextSet = new Set(prev);
if (nextSet.has(key)) nextSet.delete(key);
else nextSet.add(key);
return nextSet;
});
};
const removeEvent = (iso: string, id: string) => {
setEvents((prev) => {
const list = prev[iso];
if (!list) return prev;
const filtered = list.filter((e) => e.id !== id);
const nextEvents = { ...prev };
if (filtered.length) nextEvents[iso] = filtered;
else delete nextEvents[iso];
return nextEvents;
});
};
const submitEvent = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const title = formTitle.trim();
if (!title) return;
const item: EventItem = {
id: `${selected}#n${idRef.current++}`,
title,
time: formTime || "09:00",
category: formCat,
};
setEvents((prev) => {
const list = prev[selected] ? [...prev[selected]] : [];
return { ...prev, [selected]: [...list, item] };
});
setFormTitle("");
setFormTime("09:00");
setFormCat("work");
setShowForm(false);
};
const selectedDate = parseISO(selected);
const selectedEvents = visibleOf(selected);
return (
<section className="relative w-full bg-slate-50 px-4 py-12 dark:bg-slate-950 sm:py-16">
<style>{`
@keyframes cev-pop {
from { opacity: 0; transform: translateY(4px) scale(.995); }
to { opacity: 1; transform: none; }
}
@keyframes cev-pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: .45; transform: scale(.7); }
}
.cev-grid { animation: cev-pop .28s cubic-bezier(0.22,1,0.36,1) both; }
.cev-pulse-dot { animation: cev-pulse 2.4s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.cev-grid, .cev-pulse-dot { animation: none !important; }
}
`}</style>
<div className="mx-auto max-w-6xl">
<div className="mb-6 flex flex-col gap-1">
<span className="inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.18em] text-indigo-600 dark:text-indigo-400">
<svg
viewBox="0 0 24 24"
className="h-4 w-4"
fill="none"
stroke="currentColor"
strokeWidth="2"
aria-hidden="true"
>
<rect x="3" y="4" width="18" height="17" rx="2" />
<path d="M3 9h18M8 2v4M16 2v4" strokeLinecap="round" />
</svg>
Schedule
</span>
<h2 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-slate-50 sm:text-3xl">
Team calendar
</h2>
</div>
<div className="grid gap-6 lg:grid-cols-[1.65fr_1fr]">
{/* Calendar card */}
<div className="rounded-3xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900 sm:p-6">
{/* Toolbar */}
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
<div>
<h3
id={headingId}
aria-live="polite"
className="text-lg font-semibold text-slate-900 dark:text-slate-50"
>
{MONTHS[view.m]} {view.y}
</h3>
<p className="text-sm text-slate-500 dark:text-slate-400">
{monthCount} {monthCount === 1 ? "event" : "events"} this month
</p>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={goToday}
className="rounded-full border border-slate-200 px-3.5 py-1.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-900"
>
Today
</button>
<div className="flex items-center gap-1">
<button
type="button"
onClick={() => goMonth(-1)}
aria-label="Previous month"
className="grid h-9 w-9 place-items-center rounded-full border border-slate-200 text-slate-600 transition-colors hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-900"
>
<svg
viewBox="0 0 24 24"
className="h-5 w-5"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M15 18l-6-6 6-6" />
</svg>
</button>
<button
type="button"
onClick={() => goMonth(1)}
aria-label="Next month"
className="grid h-9 w-9 place-items-center rounded-full border border-slate-200 text-slate-600 transition-colors hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-900"
>
<svg
viewBox="0 0 24 24"
className="h-5 w-5"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M9 18l6-6-6-6" />
</svg>
</button>
</div>
</div>
</div>
{/* Grid */}
<div role="grid" aria-labelledby={headingId} className="select-none">
<div role="row" className="grid grid-cols-7">
{WEEKDAYS.map((w) => (
<div
key={w.short}
role="columnheader"
className="pb-2 text-center text-[0.7rem] font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500"
>
<abbr title={w.long} className="no-underline">
{w.short}
</abbr>
</div>
))}
</div>
<div
key={`${view.y}-${view.m}`}
className="cev-grid grid grid-rows-6 gap-1"
>
{weeks.map((week, wi) => (
<div role="row" key={wi} className="grid grid-cols-7 gap-1">
{week.map((d) => {
const iso = isoOf(d);
const inMonth = d.getMonth() === view.m;
const isToday = iso === TODAY_ISO;
const isSelected = iso === selected;
const isFocusTarget = iso === focused && inMonth;
const dayEvents = visibleOf(iso);
const count = dayEvents.length;
const label = `${formatFull(d)}${
isToday ? ", Today" : ""
}${
count > 0
? `, ${count} ${count === 1 ? "event" : "events"}`
: ", no events"
}`;
return (
<div
role="gridcell"
key={iso}
aria-selected={isSelected}
className="min-w-0"
>
<button
type="button"
ref={(el) => {
dayRefs.current[iso] = el;
}}
tabIndex={isFocusTarget ? 0 : -1}
onClick={() => pickDate(d)}
onKeyDown={(e) => onDayKeyDown(e, iso)}
aria-label={label}
aria-current={isToday ? "date" : undefined}
className={[
"group relative flex h-14 w-full flex-col items-center justify-start rounded-xl px-1 pt-1.5 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 sm:h-16",
isSelected
? "bg-indigo-600 text-white shadow-sm dark:bg-indigo-500"
: inMonth
? "text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-slate-800"
: "text-slate-300 hover:bg-slate-50 dark:text-slate-600 dark:hover:bg-slate-800/50",
!isSelected && isToday
? "ring-1 ring-inset ring-indigo-300 dark:ring-indigo-500/50"
: "",
].join(" ")}
>
<span
className={[
"grid h-6 w-6 place-items-center rounded-full text-[0.8rem] font-medium tabular-nums",
isToday && !isSelected
? "text-indigo-600 dark:text-indigo-300"
: "",
].join(" ")}
>
{d.getDate()}
</span>
<span
aria-hidden="true"
className="mt-0.5 flex h-2 items-center justify-center gap-[3px]"
>
{dayEvents.slice(0, 3).map((ev) => (
<span
key={ev.id}
className={[
"h-1.5 w-1.5 rounded-full",
isSelected
? "bg-white/90"
: META_BY_KEY[ev.category].dot,
isToday && !isSelected
? "cev-pulse-dot"
: "",
].join(" ")}
/>
))}
{count > 3 && (
<span
className={[
"ml-0.5 text-[0.6rem] font-semibold leading-none",
isSelected
? "text-white/90"
: "text-slate-400 dark:text-slate-500",
].join(" ")}
>
+{count - 3}
</span>
)}
</span>
</button>
</div>
);
})}
</div>
))}
</div>
</div>
{/* Legend / filters */}
<div className="mt-5 flex flex-wrap items-center gap-2 border-t border-slate-100 pt-4 dark:border-slate-800">
<span className="mr-1 text-xs font-medium text-slate-400 dark:text-slate-500">
Filter
</span>
{CATEGORIES.map((c) => {
const on = activeCats.has(c.key);
return (
<button
key={c.key}
type="button"
aria-pressed={on}
onClick={() => toggleCat(c.key)}
className={[
"inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900",
on
? "border-slate-200 bg-white text-slate-700 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200"
: "border-transparent bg-slate-100 text-slate-400 line-through dark:bg-slate-800/50 dark:text-slate-600",
].join(" ")}
>
<span
className={[
"h-2 w-2 rounded-full",
on ? c.dot : "bg-slate-300 dark:bg-slate-600",
].join(" ")}
aria-hidden="true"
/>
{c.label}
</button>
);
})}
</div>
</div>
{/* Side panel */}
<aside
id={panelId}
aria-label="Events for selected day"
className="flex flex-col rounded-3xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900 sm:p-6"
>
<div className="flex items-baseline justify-between gap-3">
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-indigo-600 dark:text-indigo-400">
{WEEKDAYS[selectedDate.getDay()].long}
</p>
<p className="text-xl font-bold text-slate-900 dark:text-slate-50">
{MONTHS[selectedDate.getMonth()]} {selectedDate.getDate()}
</p>
</div>
<span className="rounded-full bg-slate-100 px-2.5 py-1 text-xs font-semibold text-slate-600 dark:bg-slate-800 dark:text-slate-300">
{selectedEvents.length}{" "}
{selectedEvents.length === 1 ? "event" : "events"}
</span>
</div>
<div className="mt-4 min-h-[8rem] flex-1">
<AnimatePresence mode="wait">
<motion.ul
key={selected}
initial={reduce ? false : { opacity: 0, y: 8 }}
animate={reduce ? {} : { opacity: 1, y: 0 }}
exit={reduce ? {} : { opacity: 0, y: -8 }}
transition={{ duration: 0.22, ease: [0.22, 1, 0.36, 1] }}
className="space-y-2"
>
{selectedEvents.length === 0 ? (
<li className="flex flex-col items-center justify-center rounded-2xl border border-dashed border-slate-200 py-10 text-center dark:border-slate-700">
<svg
viewBox="0 0 24 24"
className="mb-2 h-8 w-8 text-slate-300 dark:text-slate-600"
fill="none"
stroke="currentColor"
strokeWidth="1.6"
aria-hidden="true"
>
<rect x="3" y="4" width="18" height="17" rx="2" />
<path d="M3 9h18M8 2v4M16 2v4" strokeLinecap="round" />
</svg>
<p className="text-sm text-slate-500 dark:text-slate-400">
Nothing scheduled.
</p>
</li>
) : (
selectedEvents.map((ev) => {
const meta = META_BY_KEY[ev.category];
return (
<li
key={ev.id}
className="flex items-start gap-3 rounded-2xl border border-slate-200 bg-slate-50/70 p-3 dark:border-slate-800 dark:bg-slate-800/40"
>
<span
className={`mt-1 h-10 w-1 shrink-0 rounded-full ${meta.dot}`}
aria-hidden="true"
/>
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-2">
<p className="truncate font-medium text-slate-900 dark:text-slate-100">
{ev.title}
</p>
<button
type="button"
onClick={() => removeEvent(selected, ev.id)}
aria-label={`Remove ${ev.title}`}
className="-mr-1 -mt-1 grid h-7 w-7 shrink-0 place-items-center rounded-lg text-slate-400 transition-colors hover:bg-rose-50 hover:text-rose-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-500 dark:hover:bg-rose-500/10 dark:hover:text-rose-400"
>
<svg
viewBox="0 0 24 24"
className="h-4 w-4"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6" />
</svg>
</button>
</div>
<div className="mt-1.5 flex items-center gap-2">
<span className="inline-flex items-center gap-1 text-xs text-slate-500 dark:text-slate-400">
<svg
viewBox="0 0 24 24"
className="h-3.5 w-3.5"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="9" />
<path d="M12 7v5l3 2" />
</svg>
{formatTime(ev.time)}
</span>
<span
className={`inline-flex items-center rounded-full px-2 py-0.5 text-[0.65rem] font-semibold uppercase tracking-wide ring-1 ring-inset ${meta.badge}`}
>
{meta.label}
</span>
</div>
</div>
</li>
);
})
)}
</motion.ul>
</AnimatePresence>
</div>
{/* Add event */}
<div className="mt-4 border-t border-slate-100 pt-4 dark:border-slate-800">
{!showForm ? (
<button
type="button"
onClick={() => setShowForm(true)}
aria-expanded={showForm}
className="flex w-full items-center justify-center gap-2 rounded-2xl bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:focus-visible:ring-offset-slate-900"
>
<svg
viewBox="0 0 24 24"
className="h-4 w-4"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
aria-hidden="true"
>
<path d="M12 5v14M5 12h14" />
</svg>
New event
</button>
) : (
<form onSubmit={submitEvent} className="space-y-3">
<p className="text-sm font-medium text-slate-700 dark:text-slate-300">
Add to {MONTHS[selectedDate.getMonth()].slice(0, 3)}{" "}
{selectedDate.getDate()}
</p>
<div>
<label
htmlFor={`${uid}-title`}
className="mb-1 block text-xs font-medium text-slate-500 dark:text-slate-400"
>
Title
</label>
<input
id={`${uid}-title`}
type="text"
required
value={formTitle}
onChange={(e) => setFormTitle(e.target.value)}
placeholder="Coffee with Dana"
className="w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100 dark:placeholder:text-slate-500"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label
htmlFor={`${uid}-time`}
className="mb-1 block text-xs font-medium text-slate-500 dark:text-slate-400"
>
Time
</label>
<input
id={`${uid}-time`}
type="time"
value={formTime}
onChange={(e) => setFormTime(e.target.value)}
className="w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100 [color-scheme:light] dark:[color-scheme:dark]"
/>
</div>
<div>
<label
htmlFor={`${uid}-cat`}
className="mb-1 block text-xs font-medium text-slate-500 dark:text-slate-400"
>
Category
</label>
<select
id={`${uid}-cat`}
value={formCat}
onChange={(e) =>
setFormCat(e.target.value as CategoryKey)
}
className="w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100"
>
{CATEGORIES.map((c) => (
<option key={c.key} value={c.key}>
{c.label}
</option>
))}
</select>
</div>
</div>
<div className="flex gap-2 pt-1">
<button
type="submit"
className="flex-1 rounded-xl bg-indigo-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:focus-visible:ring-offset-slate-900"
>
Save
</button>
<button
type="button"
onClick={() => {
setShowForm(false);
setFormTitle("");
}}
className="rounded-xl border border-slate-200 px-4 py-2 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-900"
>
Cancel
</button>
</div>
</form>
)}
</div>
</aside>
</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 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 Mini
Originalcompact mini calendar widget

Calendar Time Picker
Originaltime picker with hour/minute

Calendar Datetime
Originalcombined date and time picker

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

