Web InnoventixFreeCode

Calendar Inline

Original · free

always-visible inline calendar

byWeb InnoventixReact + Tailwind
calendarinlinecalendars
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/calendar-inline.json
calendar-inline.tsx
"use client";

import { useEffect, useMemo, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

type Category = "meeting" | "personal" | "deadline" | "focus" | "social" | "reminder";

type CalEvent = {
  id: string;
  date: string; // YYYY-MM-DD
  time: string; // HH:MM (24h)
  title: string;
  location: string;
  category: Category;
};

const CATEGORY_META: Record<
  Category,
  { label: string; dot: string; bar: string; chip: string }
> = {
  meeting: {
    label: "Meetings",
    dot: "bg-indigo-500",
    bar: "bg-indigo-500",
    chip: "bg-indigo-50 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300",
  },
  personal: {
    label: "Personal",
    dot: "bg-emerald-500",
    bar: "bg-emerald-500",
    chip: "bg-emerald-50 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300",
  },
  deadline: {
    label: "Deadlines",
    dot: "bg-rose-500",
    bar: "bg-rose-500",
    chip: "bg-rose-50 text-rose-700 dark:bg-rose-500/15 dark:text-rose-300",
  },
  focus: {
    label: "Focus",
    dot: "bg-violet-500",
    bar: "bg-violet-500",
    chip: "bg-violet-50 text-violet-700 dark:bg-violet-500/15 dark:text-violet-300",
  },
  social: {
    label: "Social",
    dot: "bg-sky-500",
    bar: "bg-sky-500",
    chip: "bg-sky-50 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300",
  },
  reminder: {
    label: "Reminders",
    dot: "bg-amber-500",
    bar: "bg-amber-500",
    chip: "bg-amber-50 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300",
  },
};

const EVENTS: CalEvent[] = [
  { id: "e01", date: "2026-06-29", time: "17:00", title: "Renew passport", location: "City Hall, Window 4", category: "reminder" },
  { id: "e02", date: "2026-07-02", time: "09:30", title: "Design sync with Priya", location: "Zoom · Room 2", category: "meeting" },
  { id: "e03", date: "2026-07-06", time: "14:00", title: "Q3 roadmap review", location: "HQ · Boardroom", category: "meeting" },
  { id: "e04", date: "2026-07-09", time: "11:00", title: "1:1 with Marcus", location: "Café Lumen", category: "meeting" },
  { id: "e05", date: "2026-07-11", time: "08:00", title: "Half marathon", location: "Riverside Park", category: "personal" },
  { id: "e06", date: "2026-07-14", time: "16:30", title: "User research readout", location: "Zoom", category: "meeting" },
  { id: "e07", date: "2026-07-16", time: "08:45", title: "Daily standup", location: "Slack Huddle", category: "meeting" },
  { id: "e08", date: "2026-07-16", time: "13:00", title: "Lunch with Aisha", location: "Nori Kitchen", category: "social" },
  { id: "e09", date: "2026-07-16", time: "15:30", title: "Ship Calendar v2", location: "Deploy window", category: "deadline" },
  { id: "e10", date: "2026-07-16", time: "19:00", title: "Deep work: Q3 deck", location: "Do not disturb", category: "focus" },
  { id: "e11", date: "2026-07-21", time: "10:00", title: "Board meeting", location: "HQ · 12th floor", category: "meeting" },
  { id: "e12", date: "2026-07-23", time: "18:00", title: "Team dinner", location: "Trattoria Vico", category: "social" },
  { id: "e13", date: "2026-07-24", time: "17:00", title: "Invoice due — Acme", location: "Finance portal", category: "deadline" },
  { id: "e14", date: "2026-07-28", time: "09:00", title: "Sprint planning", location: "Zoom", category: "meeting" },
  { id: "e15", date: "2026-07-30", time: "12:30", title: "Coffee with Devon", location: "Blue Bottle", category: "social" },
  { id: "e16", date: "2026-08-03", time: "15:00", title: "Dentist checkup", location: "Bright Smile Clinic", category: "personal" },
];

const TODAY = new Date(2026, 6, 16); // Thu, Jul 16 2026 — demo reference date

const WEEKDAYS: { short: string; long: string }[] = [
  { 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" },
];

function cx(...parts: Array<string | false | null | undefined>): string {
  return parts.filter(Boolean).join(" ");
}
function startOfMonth(d: Date): Date {
  return new Date(d.getFullYear(), d.getMonth(), 1);
}
function addDays(d: Date, n: number): Date {
  return new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
}
function addMonths(d: Date, n: number): Date {
  const target = new Date(d.getFullYear(), d.getMonth() + n, 1);
  const last = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
  target.setDate(Math.min(d.getDate(), last));
  return target;
}
function addYears(d: Date, n: number): Date {
  return addMonths(d, n * 12);
}
function startOfWeek(d: Date): Date {
  return addDays(d, -d.getDay());
}
function endOfWeek(d: Date): Date {
  return addDays(d, 6 - d.getDay());
}
function sameDay(a: Date, b: Date): boolean {
  return (
    a.getFullYear() === b.getFullYear() &&
    a.getMonth() === b.getMonth() &&
    a.getDate() === b.getDate()
  );
}
function sameMonth(a: Date, b: Date): boolean {
  return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
}
function toKey(d: Date): string {
  const m = String(d.getMonth() + 1).padStart(2, "0");
  const day = String(d.getDate()).padStart(2, "0");
  return `${d.getFullYear()}-${m}-${day}`;
}
function clampToMonth(d: Date, month: Date): Date {
  const last = new Date(month.getFullYear(), month.getMonth() + 1, 0).getDate();
  return new Date(month.getFullYear(), month.getMonth(), Math.min(d.getDate(), last));
}
function formatTime(t: string): string {
  const [hStr, mStr] = t.split(":");
  const h = Number(hStr);
  const period = h >= 12 ? "PM" : "AM";
  const h12 = h % 12 === 0 ? 12 : h % 12;
  return `${h12}:${mStr} ${period}`;
}

const monthFmt = new Intl.DateTimeFormat("en-US", { month: "long", year: "numeric" });
const longFmt = new Intl.DateTimeFormat("en-US", {
  weekday: "long",
  month: "long",
  day: "numeric",
  year: "numeric",
});
const agendaFmt = new Intl.DateTimeFormat("en-US", {
  weekday: "long",
  month: "long",
  day: "numeric",
});

const styles = `
@keyframes ci_pop {
  0% { transform: scale(0.82); }
  55% { transform: scale(1.06); }
  100% { transform: scale(1); }
}
@keyframes ci_pulse {
  0%, 100% { opacity: 0.35; transform: scale(1); }
  50% { opacity: 1; transform: scale(1.35); }
}
.ci-selected-pop { animation: ci_pop 0.28s ease-out; }
.ci-live-dot { animation: ci_pulse 2.4s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .ci-selected-pop, .ci-live-dot { animation: none !important; }
}
`;

function ChevronLeft() {
  return (
    <svg viewBox="0 0 24 24" fill="none" className="h-4 w-4" aria-hidden="true">
      <path d="M15 5l-7 7 7 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}
function ChevronRight() {
  return (
    <svg viewBox="0 0 24 24" fill="none" className="h-4 w-4" aria-hidden="true">
      <path d="M9 5l7 7-7 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}
function PinIcon() {
  return (
    <svg viewBox="0 0 24 24" fill="none" className="h-3.5 w-3.5 shrink-0" aria-hidden="true">
      <path d="M12 21s7-5.686 7-11a7 7 0 10-14 0c0 5.314 7 11 7 11z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" />
      <circle cx="12" cy="10" r="2.4" stroke="currentColor" strokeWidth="1.6" />
    </svg>
  );
}
function EmptyIcon() {
  return (
    <svg viewBox="0 0 24 24" fill="none" className="h-8 w-8" aria-hidden="true">
      <rect x="3" y="5" width="18" height="16" rx="2.5" stroke="currentColor" strokeWidth="1.5" />
      <path d="M3 9h18M8 3v4M16 3v4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
      <path d="M8.5 15.5l3 3M11.5 15.5l-3 3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
    </svg>
  );
}

export default function CalendarInline() {
  const reduce = useReducedMotion();

  const [viewMonth, setViewMonth] = useState<Date>(startOfMonth(TODAY));
  const [selectedDate, setSelectedDate] = useState<Date>(TODAY);
  const [focusedDate, setFocusedDate] = useState<Date>(TODAY);

  const gridRef = useRef<HTMLDivElement>(null);
  const focusFromKeyboard = useRef<boolean>(false);

  const eventsByKey = useMemo(() => {
    const map = new Map<string, CalEvent[]>();
    for (const ev of EVENTS) {
      const list = map.get(ev.date);
      if (list) list.push(ev);
      else map.set(ev.date, [ev]);
    }
    for (const list of map.values()) list.sort((a, b) => a.time.localeCompare(b.time));
    return map;
  }, []);

  const weeks = useMemo(() => {
    const start = startOfWeek(startOfMonth(viewMonth));
    const rows: Date[][] = [];
    for (let w = 0; w < 6; w++) {
      const row: Date[] = [];
      for (let d = 0; d < 7; d++) row.push(addDays(start, w * 7 + d));
      rows.push(row);
    }
    return rows;
  }, [viewMonth]);

  const selectedEvents = useMemo(
    () => eventsByKey.get(toKey(selectedDate)) ?? [],
    [eventsByKey, selectedDate],
  );

  const monthEventCount = useMemo(
    () => EVENTS.filter((e) => e.date.slice(0, 7) === toKey(viewMonth).slice(0, 7)).length,
    [viewMonth],
  );

  useEffect(() => {
    if (!focusFromKeyboard.current) return;
    focusFromKeyboard.current = false;
    const btn = gridRef.current?.querySelector<HTMLButtonElement>(
      `button[data-daykey="${toKey(focusedDate)}"]`,
    );
    btn?.focus();
  }, [focusedDate, viewMonth]);

  function moveFocus(day: Date) {
    setFocusedDate(day);
    if (!sameMonth(day, viewMonth)) setViewMonth(startOfMonth(day));
  }

  function handleSelect(day: Date) {
    setSelectedDate(day);
    setFocusedDate(day);
    if (!sameMonth(day, viewMonth)) {
      focusFromKeyboard.current = true;
      setViewMonth(startOfMonth(day));
    }
  }

  function goMonth(delta: number) {
    const nv = addMonths(viewMonth, delta);
    setViewMonth(nv);
    setFocusedDate(clampToMonth(focusedDate, nv));
  }

  function goToday() {
    setViewMonth(startOfMonth(TODAY));
    setSelectedDate(TODAY);
    setFocusedDate(TODAY);
  }

  function onGridKeyDown(e: ReactKeyboardEvent<HTMLDivElement>) {
    let next: Date | null = null;
    switch (e.key) {
      case "ArrowLeft":
        next = addDays(focusedDate, -1);
        break;
      case "ArrowRight":
        next = addDays(focusedDate, 1);
        break;
      case "ArrowUp":
        next = addDays(focusedDate, -7);
        break;
      case "ArrowDown":
        next = addDays(focusedDate, 7);
        break;
      case "Home":
        next = startOfWeek(focusedDate);
        break;
      case "End":
        next = endOfWeek(focusedDate);
        break;
      case "PageUp":
        next = e.shiftKey ? addYears(focusedDate, -1) : addMonths(focusedDate, -1);
        break;
      case "PageDown":
        next = e.shiftKey ? addYears(focusedDate, 1) : addMonths(focusedDate, 1);
        break;
      case "Enter":
      case " ":
        e.preventDefault();
        handleSelect(focusedDate);
        return;
      default:
        return;
    }
    if (next) {
      e.preventDefault();
      focusFromKeyboard.current = true;
      moveFocus(next);
    }
  }

  const captionId = "ci-month-caption";
  const monthKey = toKey(viewMonth).slice(0, 7);

  const listMotion = reduce
    ? { hidden: {}, show: {} }
    : { hidden: {}, show: { transition: { staggerChildren: 0.05, delayChildren: 0.03 } } };
  const itemMotion = reduce
    ? { hidden: { opacity: 1 }, show: { opacity: 1 } }
    : { hidden: { opacity: 0, y: 10 }, show: { opacity: 1, y: 0 } };

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 px-4 py-16 text-slate-900 sm:px-6 sm:py-20 dark:bg-slate-950 dark:text-slate-100">
      <style>{styles}</style>

      {/* atmosphere */}
      <div aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden">
        <div className="absolute -left-24 -top-32 h-80 w-80 rounded-full bg-indigo-300/40 blur-3xl dark:bg-indigo-600/15" />
        <div className="absolute -right-20 top-0 h-72 w-72 rounded-full bg-sky-300/40 blur-3xl dark:bg-violet-600/15" />
      </div>

      <div className="relative mx-auto max-w-4xl">
        <header className="mb-8">
          <div className="flex items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-indigo-600 dark:text-indigo-400">
            <span className="relative flex h-2 w-2">
              <span className="ci-live-dot absolute inline-flex h-full w-full rounded-full bg-indigo-500" />
              <span className="relative inline-flex h-2 w-2 rounded-full bg-indigo-500" />
            </span>
            Schedule
          </div>
          <h2 className="mt-3 text-2xl font-semibold tracking-tight sm:text-3xl">
            Your calendar
          </h2>
          <p className="mt-1.5 text-sm text-slate-500 dark:text-slate-400">
            Today is {longFmt.format(TODAY)}. Pick a day to see what&rsquo;s on.
          </p>
        </header>

        <div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_20rem]">
          {/* Calendar card */}
          <div className="rounded-2xl border border-slate-200 bg-white/80 p-4 shadow-sm backdrop-blur-sm sm:p-6 dark:border-slate-800 dark:bg-slate-900/70">
            <div className="mb-5 flex items-center justify-between gap-3">
              <div className="min-w-0">
                <AnimatePresence mode="wait" initial={false}>
                  <motion.h3
                    key={monthKey}
                    id={captionId}
                    className="truncate text-lg font-semibold tracking-tight"
                    initial={reduce ? false : { opacity: 0, y: -6 }}
                    animate={reduce ? {} : { opacity: 1, y: 0 }}
                    exit={reduce ? {} : { opacity: 0, y: 6 }}
                    transition={{ duration: 0.18, ease: "easeOut" }}
                  >
                    {monthFmt.format(viewMonth)}
                  </motion.h3>
                </AnimatePresence>
                <p className="mt-0.5 text-xs text-slate-500 dark:text-slate-400">
                  {monthEventCount} {monthEventCount === 1 ? "event" : "events"} this month
                </p>
              </div>

              <div className="flex items-center gap-1.5">
                <button
                  type="button"
                  onClick={goToday}
                  className="rounded-lg px-2.5 py-1.5 text-xs 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:text-slate-300 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-900"
                >
                  Today
                </button>
                <button
                  type="button"
                  onClick={() => goMonth(-1)}
                  aria-label="Previous month"
                  className="grid h-8 w-8 place-items-center rounded-lg border border-slate-200 text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 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:hover:text-white dark:focus-visible:ring-offset-slate-900"
                >
                  <ChevronLeft />
                </button>
                <button
                  type="button"
                  onClick={() => goMonth(1)}
                  aria-label="Next month"
                  className="grid h-8 w-8 place-items-center rounded-lg border border-slate-200 text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 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:hover:text-white dark:focus-visible:ring-offset-slate-900"
                >
                  <ChevronRight />
                </button>
              </div>
            </div>

            <div
              ref={gridRef}
              role="grid"
              aria-labelledby={captionId}
              onKeyDown={onGridKeyDown}
              className="select-none"
            >
              {/* weekday header */}
              <div role="row" className="grid grid-cols-7 gap-1">
                {WEEKDAYS.map((w) => (
                  <div
                    key={w.long}
                    role="columnheader"
                    aria-label={w.long}
                    className="pb-2 text-center text-[0.68rem] font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500"
                  >
                    {w.short}
                  </div>
                ))}
              </div>

              {/* weeks */}
              <div role="rowgroup" className="space-y-1">
                {weeks.map((week, wi) => (
                  <div role="row" key={wi} className="grid grid-cols-7 gap-1">
                    {week.map((day) => {
                      const key = toKey(day);
                      const inMonth = sameMonth(day, viewMonth);
                      const isSelected = sameDay(day, selectedDate);
                      const isToday = sameDay(day, TODAY);
                      const isFocusTarget = sameDay(day, focusedDate);
                      const dayEvents = eventsByKey.get(key) ?? [];
                      const shownDots = dayEvents.slice(0, 3);
                      const label =
                        longFmt.format(day) +
                        (dayEvents.length
                          ? `, ${dayEvents.length} ${dayEvents.length === 1 ? "event" : "events"}`
                          : ", no events");

                      return (
                        <div
                          role="gridcell"
                          aria-selected={isSelected}
                          key={key}
                          className="relative"
                        >
                          <button
                            type="button"
                            data-daykey={key}
                            tabIndex={isFocusTarget ? 0 : -1}
                            aria-label={label}
                            aria-current={isToday ? "date" : undefined}
                            onClick={() => handleSelect(day)}
                            className={cx(
                              "group relative flex aspect-square w-full flex-col items-center justify-center rounded-xl text-sm font-medium transition-colors 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",
                              isSelected
                                ? "ci-selected-pop bg-indigo-600 text-white shadow-md shadow-indigo-600/25 dark:bg-indigo-500"
                                : isToday
                                  ? "font-semibold text-indigo-700 ring-1 ring-inset ring-indigo-300 hover:bg-indigo-50 dark:text-indigo-300 dark:ring-indigo-500/40 dark:hover:bg-indigo-500/10"
                                  : inMonth
                                    ? "text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-slate-800"
                                    : "text-slate-300 hover:bg-slate-100 dark:text-slate-600 dark:hover:bg-slate-800/60",
                            )}
                          >
                            <span className="leading-none tabular-nums">{day.getDate()}</span>

                            {/* event dots */}
                            <span className="absolute bottom-1.5 flex items-center gap-0.5">
                              {shownDots.map((ev) => (
                                <span
                                  key={ev.id}
                                  className={cx(
                                    "h-1.5 w-1.5 rounded-full",
                                    isSelected ? "bg-white/85" : CATEGORY_META[ev.category].dot,
                                  )}
                                />
                              ))}
                              {dayEvents.length > 3 && (
                                <span
                                  className={cx(
                                    "ml-0.5 text-[0.55rem] font-semibold leading-none",
                                    isSelected ? "text-white/85" : "text-slate-400 dark:text-slate-500",
                                  )}
                                >
                                  +{dayEvents.length - 3}
                                </span>
                              )}
                            </span>
                          </button>
                        </div>
                      );
                    })}
                  </div>
                ))}
              </div>
            </div>

            {/* legend */}
            <div className="mt-5 flex flex-wrap items-center gap-x-4 gap-y-2 border-t border-slate-100 pt-4 dark:border-slate-800">
              {(Object.keys(CATEGORY_META) as Category[]).map((cat) => (
                <span key={cat} className="flex items-center gap-1.5 text-[0.7rem] text-slate-500 dark:text-slate-400">
                  <span className={cx("h-2 w-2 rounded-full", CATEGORY_META[cat].dot)} />
                  {CATEGORY_META[cat].label}
                </span>
              ))}
            </div>
          </div>

          {/* Agenda card */}
          <aside
            aria-label={`Agenda for ${longFmt.format(selectedDate)}`}
            className="flex flex-col rounded-2xl border border-slate-200 bg-white/80 p-5 shadow-sm backdrop-blur-sm dark:border-slate-800 dark:bg-slate-900/70"
          >
            <div className="mb-4">
              <p className="text-xs font-medium uppercase tracking-wide text-indigo-600 dark:text-indigo-400">
                {sameDay(selectedDate, TODAY) ? "Today" : "Selected"}
              </p>
              <h3 className="mt-1 text-base font-semibold tracking-tight">
                {agendaFmt.format(selectedDate)}
              </h3>
              <p className="mt-0.5 text-xs text-slate-500 dark:text-slate-400">
                {selectedEvents.length
                  ? `${selectedEvents.length} ${selectedEvents.length === 1 ? "event" : "events"} scheduled`
                  : "Nothing scheduled"}
              </p>
            </div>

            <div aria-live="polite" className="min-h-0 flex-1">
              {selectedEvents.length === 0 ? (
                <div className="flex h-full flex-col items-center justify-center rounded-xl border border-dashed border-slate-200 py-10 text-center text-slate-400 dark:border-slate-700 dark:text-slate-500">
                  <EmptyIcon />
                  <p className="mt-2 text-sm font-medium text-slate-500 dark:text-slate-400">
                    A clear day
                  </p>
                  <p className="mt-0.5 text-xs">No events on this date.</p>
                </div>
              ) : (
                <motion.ul
                  key={toKey(selectedDate)}
                  variants={listMotion}
                  initial="hidden"
                  animate="show"
                  className="space-y-2.5"
                >
                  {selectedEvents.map((ev) => {
                    const meta = CATEGORY_META[ev.category];
                    return (
                      <motion.li
                        key={ev.id}
                        variants={itemMotion}
                        className="flex gap-3 rounded-xl border border-slate-200 bg-slate-50/70 p-3 dark:border-slate-800 dark:bg-slate-800/40"
                      >
                        <span className={cx("w-1 shrink-0 self-stretch rounded-full", meta.bar)} />
                        <div className="min-w-0 flex-1">
                          <div className="flex items-center justify-between gap-2">
                            <span className="text-xs font-semibold tabular-nums text-slate-500 dark:text-slate-400">
                              {formatTime(ev.time)}
                            </span>
                            <span className={cx("rounded-full px-2 py-0.5 text-[0.62rem] font-medium", meta.chip)}>
                              {meta.label.replace(/s$/, "")}
                            </span>
                          </div>
                          <p className="mt-1 truncate text-sm font-semibold text-slate-800 dark:text-slate-100">
                            {ev.title}
                          </p>
                          <p className="mt-0.5 flex items-center gap-1 text-xs text-slate-500 dark:text-slate-400">
                            <PinIcon />
                            <span className="truncate">{ev.location}</span>
                          </p>
                        </div>
                      </motion.li>
                    );
                  })}
                </motion.ul>
              )}
            </div>

            <p className="mt-4 border-t border-slate-100 pt-3 text-[0.7rem] leading-relaxed text-slate-400 dark:border-slate-800 dark:text-slate-500">
              Tip: use arrow keys to move by day, <kbd className="rounded border border-slate-300 px-1 font-sans dark:border-slate-600">PageUp</kbd>/<kbd className="rounded border border-slate-300 px-1 font-sans dark:border-slate-600">PageDown</kbd> to change month, and <kbd className="rounded border border-slate-300 px-1 font-sans dark:border-slate-600">Enter</kbd> to select.
            </p>
          </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 quote

Similar components

Browse all →