Components
Interactive Calendar
Interactive Calendar
An interactive calendar component with animated date selection and event indicators.
January 2024
S
M
T
W
T
F
S
Events
Today
Installation
1
Install the packages
npm i motion react-icons2
Add util file
lib/util.ts
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
3
Copy and paste the following code into your project
interactive-calendar.tsx
"use client";
import { cn } from "@/lib/utils";
import { motion } from "motion/react";
import React, { useEffect, useState } from "react";
import { FiChevronLeft, FiChevronRight, FiCalendar } from "react-icons/fi";
import ComponentContainer from "@/components/features/component-container";
const InteractiveCalendar = () => {
const [animationKey, setAnimationKey] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setAnimationKey((prev) => prev + 1);
}, 12000);
return () => clearInterval(interval);
});
return (
<ComponentContainer className="md:py-20">
<CalendarAnimation key={animationKey} />
</ComponentContainer>
);
};
export default InteractiveCalendar;
const CalendarAnimation = () => {
const [selectedDate, setSelectedDate] = useState(15);
const [currentMonth, setCurrentMonth] = useState(0);
const months = ["January", "February", "March", "April", "May", "June"];
const days = ["S", "M", "T", "W", "T", "F", "S"];
const dates = Array.from({ length: 31 }, (_, i) => i + 1);
const events = [5, 12, 18, 25]; // Days with events
useEffect(() => {
const interval = setInterval(() => {
setSelectedDate((prev) => (prev % 31) + 1);
}, 2000);
return () => clearInterval(interval);
}, []);
return (
<div
className={cn(
"relative",
"flex h-[14rem] w-full max-w-[350px] flex-col items-center justify-center",
"rounded-md border border-neutral-800 bg-neutral-900 p-6",
)}
>
{/* Header */}
<div className="mb-4 flex w-full items-center justify-between">
<motion.button
className="flex h-8 w-8 items-center justify-center rounded-full bg-neutral-800 hover:bg-neutral-700"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => setCurrentMonth((prev) => (prev - 1 + months.length) % months.length)}
>
<FiChevronLeft className="h-4 w-4 text-neutral-400" />
</motion.button>
<div className="flex items-center gap-2">
<FiCalendar className="h-4 w-4 text-cyan-400" />
<span className="text-sm font-semibold text-white">
{months[currentMonth]} 2024
</span>
</div>
<motion.button
className="flex h-8 w-8 items-center justify-center rounded-full bg-neutral-800 hover:bg-neutral-700"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => setCurrentMonth((prev) => (prev + 1) % months.length)}
>
<FiChevronRight className="h-4 w-4 text-neutral-400" />
</motion.button>
</div>
{/* Days of week */}
<div className="mb-2 grid w-full grid-cols-7 gap-1">
{days.map((day, dayIndex) => (
<div
key={`day-${dayIndex}`}
className="flex h-6 items-center justify-center text-xs font-medium text-neutral-500"
>
{day}
</div>
))}
</div>
{/* Calendar Grid */}
<div className="grid w-full grid-cols-7 gap-1">
{dates.map((date, index) => {
const hasEvent = events.includes(date);
const isSelected = date === selectedDate;
const isToday = date === 15;
return (
<motion.button
key={`date-${date}`}
className={cn(
"relative flex h-8 w-8 items-center justify-center rounded-lg text-xs font-medium transition-colors",
isSelected
? "bg-cyan-500 text-white shadow-lg shadow-cyan-500/30"
: isToday
? "bg-neutral-700 text-cyan-400"
: "bg-neutral-800 text-neutral-300 hover:bg-neutral-700"
)}
initial={{ opacity: 0, scale: 0.8 }}
animate={{
opacity: 1,
scale: isSelected ? 1.1 : 1,
}}
transition={{
duration: 0.3,
delay: index * 0.02,
ease: "easeOut",
}}
whileHover={{ scale: isSelected ? 1.1 : 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => setSelectedDate(date)}
>
{date}
{/* Event indicator */}
{hasEvent && (
<motion.div
className="absolute -bottom-1 h-1 w-1 rounded-full bg-orange-400"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ duration: 0.3, delay: index * 0.05 }}
/>
)}
{/* Selection glow */}
{isSelected && (
<motion.div
className="absolute inset-0 rounded-lg bg-cyan-400/20"
initial={{ opacity: 0 }}
animate={{ opacity: [0, 0.5, 0] }}
transition={{ duration: 2, ease: "easeInOut", repeat: Infinity }}
/>
)}
</motion.button>
);
})}
</div>
{/* Legend */}
<div className="mt-4 flex items-center justify-center gap-4 text-xs text-neutral-500">
<div key="events-legend" className="flex items-center gap-1">
<div className="h-2 w-2 rounded-full bg-orange-400" />
<span>Events</span>
</div>
<div key="today-legend" className="flex items-center gap-1">
<div className="h-2 w-2 rounded-full bg-cyan-400" />
<span>Today</span>
</div>
</div>
</div>
);
};
"use client";
import { cn } from "@/lib/utils";
import { motion } from "motion/react";
import React, { useEffect, useState } from "react";
import { FiChevronLeft, FiChevronRight, FiCalendar } from "react-icons/fi";
import ComponentContainer from "@/components/features/component-container";
const InteractiveCalendar = () => {
const [animationKey, setAnimationKey] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setAnimationKey((prev) => prev + 1);
}, 12000);
return () => clearInterval(interval);
});
return (
<ComponentContainer className="md:py-20">
<CalendarAnimation key={animationKey} />
</ComponentContainer>
);
};
export default InteractiveCalendar;
const CalendarAnimation = () => {
const [selectedDate, setSelectedDate] = useState(15);
const [currentMonth, setCurrentMonth] = useState(0);
const months = ["January", "February", "March", "April", "May", "June"];
const days = ["S", "M", "T", "W", "T", "F", "S"];
const dates = Array.from({ length: 31 }, (_, i) => i + 1);
const events = [5, 12, 18, 25]; // Days with events
useEffect(() => {
const interval = setInterval(() => {
setSelectedDate((prev) => (prev % 31) + 1);
}, 2000);
return () => clearInterval(interval);
}, []);
return (
<div
className={cn(
"relative",
"flex h-[14rem] w-full max-w-[350px] flex-col items-center justify-center",
"rounded-md border border-neutral-800 bg-neutral-900 p-6",
)}
>
{/* Header */}
<div className="mb-4 flex w-full items-center justify-between">
<motion.button
className="flex h-8 w-8 items-center justify-center rounded-full bg-neutral-800 hover:bg-neutral-700"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => setCurrentMonth((prev) => (prev - 1 + months.length) % months.length)}
>
<FiChevronLeft className="h-4 w-4 text-neutral-400" />
</motion.button>
<div className="flex items-center gap-2">
<FiCalendar className="h-4 w-4 text-cyan-400" />
<span className="text-sm font-semibold text-white">
{months[currentMonth]} 2024
</span>
</div>
<motion.button
className="flex h-8 w-8 items-center justify-center rounded-full bg-neutral-800 hover:bg-neutral-700"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => setCurrentMonth((prev) => (prev + 1) % months.length)}
>
<FiChevronRight className="h-4 w-4 text-neutral-400" />
</motion.button>
</div>
{/* Days of week */}
<div className="mb-2 grid w-full grid-cols-7 gap-1">
{days.map((day, dayIndex) => (
<div
key={`day-${dayIndex}`}
className="flex h-6 items-center justify-center text-xs font-medium text-neutral-500"
>
{day}
</div>
))}
</div>
{/* Calendar Grid */}
<div className="grid w-full grid-cols-7 gap-1">
{dates.map((date, index) => {
const hasEvent = events.includes(date);
const isSelected = date === selectedDate;
const isToday = date === 15;
return (
<motion.button
key={`date-${date}`}
className={cn(
"relative flex h-8 w-8 items-center justify-center rounded-lg text-xs font-medium transition-colors",
isSelected
? "bg-cyan-500 text-white shadow-lg shadow-cyan-500/30"
: isToday
? "bg-neutral-700 text-cyan-400"
: "bg-neutral-800 text-neutral-300 hover:bg-neutral-700"
)}
initial={{ opacity: 0, scale: 0.8 }}
animate={{
opacity: 1,
scale: isSelected ? 1.1 : 1,
}}
transition={{
duration: 0.3,
delay: index * 0.02,
ease: "easeOut",
}}
whileHover={{ scale: isSelected ? 1.1 : 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => setSelectedDate(date)}
>
{date}
{/* Event indicator */}
{hasEvent && (
<motion.div
className="absolute -bottom-1 h-1 w-1 rounded-full bg-orange-400"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ duration: 0.3, delay: index * 0.05 }}
/>
)}
{/* Selection glow */}
{isSelected && (
<motion.div
className="absolute inset-0 rounded-lg bg-cyan-400/20"
initial={{ opacity: 0 }}
animate={{ opacity: [0, 0.5, 0] }}
transition={{ duration: 2, ease: "easeInOut", repeat: Infinity }}
/>
)}
</motion.button>
);
})}
</div>
{/* Legend */}
<div className="mt-4 flex items-center justify-center gap-4 text-xs text-neutral-500">
<div key="events-legend" className="flex items-center gap-1">
<div className="h-2 w-2 rounded-full bg-orange-400" />
<span>Events</span>
</div>
<div key="today-legend" className="flex items-center gap-1">
<div className="h-2 w-2 rounded-full bg-cyan-400" />
<span>Today</span>
</div>
</div>
</div>
);
};
4
Update the import paths to match your project setup
Props
| Prop | Type | Default | Description |
|---|