Week Strip Calendar
Free Calender React component — WeekStripCalendar. MIT licensed, copy-paste ready for Next.js and Tailwind CSS.
Horizontal week strip for booking and scheduling apps — arrow through weeks, tap a day to select.
Preview
September 2026
Aug 30 – Sep 5
Setup
How to use
Free for personal and commercial use. No UI attribution required. Include the MIT copyright notice when copying component source into your project. Read the MIT license.
- 1
Run in your terminal:
$npm install clsx tailwind-merge lucide-react - 2
Copy
lib/cn.tsbelow. Skip if you already havecn(). - 3
Copy the code below and create
components/calender/week-strip-calendar.tsxin your project. - 4
Import and render:
Exampleimport { WeekStripCalendar } from "@/components/calender/week-strip-calendar";
lib/cn.ts
import { type ClassValue, clsx } from "clsx";import { twMerge } from "tailwind-merge";export function cn(...inputs: ClassValue[]) {return twMerge(clsx(inputs));}
components/calender/week-strip-calendar.tsx
"use client";import {forwardRef,useMemo,useState,type ComponentPropsWithoutRef,} from "react";import { cn } from "@/lib/cn";import { ChevronLeft, ChevronRight } from "lucide-react";function startOfWeek(date: Date) {const copy = new Date(date);copy.setDate(copy.getDate() - copy.getDay());copy.setHours(0, 0, 0, 0);return copy;}function addDays(date: Date, days: number) {const copy = new Date(date);copy.setDate(copy.getDate() + days);return copy;}function startOfDay(date: Date) {const copy = new Date(date);copy.setHours(0, 0, 0, 0);return copy;}function isSameDate(a: Date, b: Date) {return (a.getFullYear() === b.getFullYear() &&a.getMonth() === b.getMonth() &&a.getDate() === b.getDate());}export type WeekStripCalendarProps = Readonly<{onSelect?: (date: Date) => void;} & ComponentPropsWithoutRef<"div">>;// Week strip — swipe weeks with arrows, tap a day to select it.export const WeekStripCalendar = forwardRef<HTMLDivElement, WeekStripCalendarProps>(({ className, onSelect, ...props }, ref) => {const today = useMemo(() => startOfDay(new Date()), []);const [weekStart, setWeekStart] = useState(() => startOfWeek(today));const [selected, setSelected] = useState(today);const [slideDirection, setSlideDirection] = useState(0);const days = useMemo(() => Array.from({ length: 7 }, (_, index) => addDays(weekStart, index)),[weekStart],);const monthLabel = days[3]?.toLocaleDateString("en-US", {month: "long",year: "numeric",});const rangeLabel = `${days[0]?.toLocaleDateString("en-US", { month: "short", day: "numeric" })} – ${days[6]?.toLocaleDateString("en-US", { month: "short", day: "numeric" })}`;const selectDay = (date: Date) => {setSelected(startOfDay(date));onSelect?.(startOfDay(date));};const goPrevWeek = () => {setSlideDirection(-1);setWeekStart((prev) => addDays(prev, -7));};const goNextWeek = () => {setSlideDirection(1);setWeekStart((prev) => addDays(prev, 7));};return (<divref={ref}data-slot="week-strip-calendar"className={cn("w-80 overflow-hidden rounded-2xl border border-neutral-100 bg-white px-5 py-4 font-sans select-none","shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_1px_2px_rgba(0,0,0,0.05),0_4px_8px_rgba(0,0,0,0.06),0_12px_24px_rgba(0,0,0,0.04)]",className,)}{...props}><div className="mb-1 flex items-center justify-between"><p className="text-sm font-semibold text-neutral-900">{monthLabel}</p><div className="flex items-center gap-0.5"><buttontype="button"aria-label="Previous week"onClick={goPrevWeek}className="flex size-8 cursor-pointer items-center justify-center rounded-full text-neutral-500 outline-none transition-colors duration-150 hover:bg-neutral-100 hover:text-neutral-900 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-neutral-800"><ChevronLeft size={16} strokeWidth={2} aria-hidden /></button><buttontype="button"aria-label="Next week"onClick={goNextWeek}className="flex size-8 cursor-pointer items-center justify-center rounded-full text-neutral-500 outline-none transition-colors duration-150 hover:bg-neutral-100 hover:text-neutral-900 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-neutral-800"><ChevronRight size={16} strokeWidth={2} aria-hidden /></button></div></div><pkey={rangeLabel}className="mb-4 text-xs font-medium text-neutral-500 opacity-100 transition-opacity duration-300 ease-out starting:opacity-0">{rangeLabel}</p><divkey={weekStart.toISOString()}className={cn("grid grid-cols-7 opacity-100 transition-all duration-300 ease-[cubic-bezier(0.25,0.46,0.45,0.94)] starting:opacity-0",slideDirection >= 0? "starting:translate-x-2": "starting:-translate-x-2",)}>{days.map((date) => {const isSelected = isSameDate(date, selected);const isToday = isSameDate(date, today);return (<buttonkey={date.toISOString()}type="button"aria-pressed={isSelected}onClick={() => selectDay(date)}className="group flex cursor-pointer flex-col items-center gap-1.5 outline-none"><spanclassName={cn("text-[10px] font-medium tracking-wide uppercase",isSelected ? "text-neutral-800" : "text-neutral-400",)}>{date.toLocaleDateString("en-US", { weekday: "narrow" })}</span><spanclassName={cn("relative flex size-9 items-center justify-center rounded-full text-sm font-medium tabular-nums","transition-colors duration-150","group-focus-visible:outline-2 group-focus-visible:outline-offset-1 group-focus-visible:outline-neutral-800",isSelected && "bg-neutral-800 font-semibold text-white",!isSelected &&isToday &&"bg-neutral-100 font-semibold text-neutral-800 group-hover:bg-neutral-200",!isSelected &&!isToday &&"text-neutral-700 group-hover:bg-neutral-100",)}>{date.getDate()}{isToday && !isSelected && (<spanaria-hiddenclassName="absolute -bottom-0.5 left-1/2 size-1 -translate-x-1/2 rounded-full bg-neutral-800"/>)}</span></button>);})}</div></div>);},);WeekStripCalendar.displayName = "WeekStripCalendar";