Calendar with Date Range
A beautiful calendar component that highlights a range of dates with smart styling.
Preview
Generate with AI
Want to create this component using AI? Copy the prompt below and paste it into any LLM (ChatGPT, Claude, etc.):
Installation
npx shadcn@latest add https://uiregistry.cappychat.com/registry/calendar-range.jsonInstall dependencies
npm i framer-motion lucide-react clsx tailwind-mergeAdd util file
lib/utils.ts
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}Copy the source code
components/ui/calendar-range.tsx
src/components/components/calenderRange.tsx
"use client";
import React, { useState } from "react";
import { motion } from "framer-motion";
import { ChevronLeft, ChevronRight, Check } from "lucide-react";
import { cn } from "@/lib/utils";
const DAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
const MONTHS = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
interface CalendarRangeProps {
className?: string;
daysBeforeCurrent?: number;
daysAfterCurrent?: number;
}
interface DayCell {
day: number;
month: number;
year: number;
isCurrentMonth: boolean;
}
export const CalendarRange = ({
className,
daysBeforeCurrent = 2,
daysAfterCurrent = 2,
}: CalendarRangeProps) => {
const today = new Date();
const [currentMonth, setCurrentMonth] = useState(today.getMonth());
const [currentYear, setCurrentYear] = useState(today.getFullYear());
// Calculate range dates
const rangeStart = new Date(today);
rangeStart.setDate(today.getDate() - daysBeforeCurrent);
const rangeEnd = new Date(today);
rangeEnd.setDate(today.getDate() + daysAfterCurrent);
const getDaysInMonth = (month: number, year: number) => {
return new Date(year, month + 1, 0).getDate();
};
const getFirstDayOfMonth = (month: number, year: number) => {
return new Date(year, month, 1).getDay();
};
const isDateInRange = (day: number, month: number, year: number) => {
const date = new Date(year, month, day);
date.setHours(0, 0, 0, 0);
const start = new Date(rangeStart);
start.setHours(0, 0, 0, 0);
const end = new Date(rangeEnd);
end.setHours(0, 0, 0, 0);
return date >= start && date <= end;
};
const isToday = (day: number, month: number, year: number) => {
return (
day === today.getDate() &&
month === today.getMonth() &&
year === today.getFullYear()
);
};
const isRangeStart = (day: number, month: number, year: number) => {
const date = new Date(year, month, day);
date.setHours(0, 0, 0, 0);
const start = new Date(rangeStart);
start.setHours(0, 0, 0, 0);
return date.getTime() === start.getTime();
};
const isRangeEnd = (day: number, month: number, year: number) => {
const date = new Date(year, month, day);
date.setHours(0, 0, 0, 0);
const end = new Date(rangeEnd);
end.setHours(0, 0, 0, 0);
return date.getTime() === end.getTime();
};
const handlePrevMonth = () => {
if (currentMonth === 0) {
setCurrentMonth(11);
setCurrentYear(currentYear - 1);
} else {
setCurrentMonth(currentMonth - 1);
}
};
const handleNextMonth = () => {
if (currentMonth === 11) {
setCurrentMonth(0);
setCurrentYear(currentYear + 1);
} else {
setCurrentMonth(currentMonth + 1);
}
};
const daysInMonth = getDaysInMonth(currentMonth, currentYear);
const firstDay = getFirstDayOfMonth(currentMonth, currentYear);
// Get previous month info
const prevMonth = currentMonth === 0 ? 11 : currentMonth - 1;
const prevMonthYear = currentMonth === 0 ? currentYear - 1 : currentYear;
const daysInPrevMonth = getDaysInMonth(prevMonth, prevMonthYear);
// Get next month info
const nextMonth = currentMonth === 11 ? 0 : currentMonth + 1;
const nextMonthYear = currentMonth === 11 ? currentYear + 1 : currentYear;
const days: DayCell[] = [];
// Days from previous month
for (let i = firstDay - 1; i >= 0; i--) {
days.push({
day: daysInPrevMonth - i,
month: prevMonth,
year: prevMonthYear,
isCurrentMonth: false,
});
}
// Days of current month
for (let i = 1; i <= daysInMonth; i++) {
days.push({
day: i,
month: currentMonth,
year: currentYear,
isCurrentMonth: true,
});
}
// Days from next month to fill the grid
const remainingCells = 42 - days.length; // 6 rows * 7 days
for (let i = 1; i <= remainingCells; i++) {
days.push({
day: i,
month: nextMonth,
year: nextMonthYear,
isCurrentMonth: false,
});
}
return (
<div
className={cn(
"mx-auto w-full max-w-md rounded-3xl border border-fd-border bg-fd-background p-4 md:p-8 shadow-lg",
className
)}
>
{/* Header with navigation */}
<div className="mb-8 flex items-center justify-between">
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
onClick={handlePrevMonth}
className="flex h-10 w-10 items-center justify-center rounded-lg text-fd-muted-foreground transition-colors hover:bg-fd-accent hover:text-fd-foreground"
aria-label="Previous month"
>
<ChevronLeft className="h-6 w-6" />
</motion.button>
<motion.span
key={`${currentMonth}-${currentYear}`}
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="text-xl font-bold tracking-wider text-fd-foreground"
>
{MONTHS[currentMonth]} {currentYear}
</motion.span>
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
onClick={handleNextMonth}
className="flex h-10 w-10 items-center justify-center rounded-lg text-fd-muted-foreground transition-colors hover:bg-fd-accent hover:text-fd-foreground"
aria-label="Next month"
>
<ChevronRight className="h-6 w-6" />
</motion.button>
</div>
{/* Day headers */}
<div className="mb-4 grid grid-cols-7 gap-1.5 md:gap-2">
{DAYS.map((day, index) => (
<div
key={index}
className="flex h-10 items-center justify-center text-sm font-medium text-fd-muted-foreground"
>
{day}
</div>
))}
</div>
{/* Calendar grid */}
<motion.div
key={`${currentMonth}-${currentYear}-grid`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
className="grid grid-cols-7 gap-1.5 md:gap-2"
>
{days.map((dayCell, index) => {
const inRange = isDateInRange(
dayCell.day,
dayCell.month,
dayCell.year
);
const isTodayDate = isToday(dayCell.day, dayCell.month, dayCell.year);
const isStart = isRangeStart(
dayCell.day,
dayCell.month,
dayCell.year
);
const isEnd = isRangeEnd(dayCell.day, dayCell.month, dayCell.year);
const isMiddle = inRange && !isStart && !isEnd && !isTodayDate;
return (
<motion.div
key={index}
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: index * 0.01, duration: 0.2 }}
className={cn(
"relative flex h-10 md:h-12 items-center justify-center rounded-lg text-sm md:text-lg font-medium transition-all",
!dayCell.isCurrentMonth && "text-fd-muted-foreground/40",
dayCell.isCurrentMonth &&
!inRange &&
"text-fd-foreground hover:bg-fd-accent",
isMiddle && "bg-fd-accent/50 text-fd-foreground",
(isStart || isEnd) &&
"bg-fd-primary/90 text-fd-primary-foreground shadow-md",
isTodayDate &&
"bg-fd-accent/50 text-fd-primary-foreground shadow-md"
)}
>
{dayCell.day}
{isTodayDate && (
<motion.div
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.3, duration: 0.3 }}
className="absolute inset-0 flex items-center justify-center rounded-lg "
>
<Check className="h-6 w-6 text-fd-foreground" />
</motion.div>
)}
</motion.div>
);
})}
</motion.div>
</div>
);
};
export default CalendarRange;
Usage
import { CalendarRange } from "@/components/ui/calendar-range";
export default function MyComponent() {
return <CalendarRange daysBeforeCurrent={2} daysAfterCurrent={2} />;
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | undefined | Additional CSS classes |
daysBeforeCurrent | number | 2 | Number of days before current date to highlight |
daysAfterCurrent | number | 2 | Number of days after current date to highlight |
