Mini Calendar
Compact date picker component for React and Next.js applications. Built with TypeScript support, Tailwind CSS styling, and shadcn/ui design featuring horizontal layout, navigation controls, and accessibility features.
'use client';import { MiniCalendar, MiniCalendarDay, MiniCalendarDays, MiniCalendarNavigation,} from '@/components/ui/shadcn-io/mini-calendar';const Example = () => ( <MiniCalendar> <MiniCalendarNavigation direction="prev" /> <MiniCalendarDays> {(date) => <MiniCalendarDay date={date} key={date.toISOString()} />} </MiniCalendarDays> <MiniCalendarNavigation direction="next" /> </MiniCalendar>);export default Example;
'use client';import { useControllableState } from '@radix-ui/react-use-controllable-state';import { addDays, format, isSameDay, isToday } from 'date-fns';import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';import { Slot } from 'radix-ui';import { type ButtonHTMLAttributes, type ComponentProps, createContext, type HTMLAttributes, type MouseEventHandler, type ReactNode, useContext,} from 'react';import { Button } from '@/components/ui/button';import { cn } from '@/lib/utils';// Context for sharing state between componentstype MiniCalendarContextType = { selectedDate: Date | null | undefined; onDateSelect: (date: Date) => void; startDate: Date; onNavigate: (direction: 'prev' | 'next') => void; days: number;};const MiniCalendarContext = createContext<MiniCalendarContextType | null>(null);const useMiniCalendar = () => { const context = useContext(MiniCalendarContext); if (!context) { throw new Error('MiniCalendar components must be used within MiniCalendar'); } return context;};// Helper function to get array of consecutive datesconst getDays = (startDate: Date, count: number): Date[] => { const days: Date[] = []; for (let i = 0; i < count; i++) { days.push(addDays(startDate, i)); } return days;};// Helper function to format dateconst formatDate = (date: Date) => { const month = format(date, 'MMM'); const day = format(date, 'd'); return { month, day };};export type MiniCalendarProps = HTMLAttributes<HTMLDivElement> & { value?: Date; defaultValue?: Date; onValueChange?: (date: Date | undefined) => void; startDate?: Date; defaultStartDate?: Date; onStartDateChange?: (date: Date | undefined) => void; days?: number;};export const MiniCalendar = ({ value, defaultValue, onValueChange, startDate, defaultStartDate = new Date(), onStartDateChange, days = 5, className, children, ...props}: MiniCalendarProps) => { const [selectedDate, setSelectedDate] = useControllableState< Date | undefined >({ prop: value, defaultProp: defaultValue, onChange: onValueChange, }); const [currentStartDate, setCurrentStartDate] = useControllableState({ prop: startDate, defaultProp: defaultStartDate, onChange: onStartDateChange, }); const handleDateSelect = (date: Date) => { setSelectedDate(date); }; const handleNavigate = (direction: 'prev' | 'next') => { const newStartDate = addDays( currentStartDate || new Date(), direction === 'next' ? days : -days ); setCurrentStartDate(newStartDate); }; const contextValue: MiniCalendarContextType = { selectedDate: selectedDate || null, onDateSelect: handleDateSelect, startDate: currentStartDate || new Date(), onNavigate: handleNavigate, days, }; return ( <MiniCalendarContext.Provider value={contextValue}> <div className={cn( 'flex items-center gap-2 rounded-lg border bg-background p-2', className )} {...props} > {children} </div> </MiniCalendarContext.Provider> );};export type MiniCalendarNavigationProps = ButtonHTMLAttributes<HTMLButtonElement> & { direction: 'prev' | 'next'; asChild?: boolean; };export const MiniCalendarNavigation = ({ direction, asChild = false, children, onClick, ...props}: MiniCalendarNavigationProps) => { const { onNavigate } = useMiniCalendar(); const Icon = direction === 'prev' ? ChevronLeftIcon : ChevronRightIcon; const handleClick: MouseEventHandler<HTMLButtonElement> = (event) => { onNavigate(direction); onClick?.(event); }; if (asChild) { return ( <Slot.Root onClick={handleClick} {...props}> {children} </Slot.Root> ); } return ( <Button onClick={handleClick} size={asChild ? undefined : 'icon'} type="button" variant={asChild ? undefined : 'ghost'} {...props} > {children ?? <Icon className="size-4" />} </Button> );};export type MiniCalendarDaysProps = Omit< HTMLAttributes<HTMLDivElement>, 'children'> & { children: (date: Date) => ReactNode;};export const MiniCalendarDays = ({ className, children, ...props}: MiniCalendarDaysProps) => { const { startDate, days: dayCount } = useMiniCalendar(); const days = getDays(startDate, dayCount); return ( <div className={cn('flex items-center gap-1', className)} {...props}> {days.map((date) => children(date))} </div> );};export type MiniCalendarDayProps = ComponentProps<typeof Button> & { date: Date;};export const MiniCalendarDay = ({ date, className, ...props}: MiniCalendarDayProps) => { const { selectedDate, onDateSelect } = useMiniCalendar(); const { month, day } = formatDate(date); const isSelected = selectedDate && isSameDay(date, selectedDate); const isTodayDate = isToday(date); return ( <Button className={cn( 'h-auto min-w-[3rem] flex-col gap-0 p-2 text-xs', isTodayDate && !isSelected && 'bg-accent', className )} onClick={() => onDateSelect(date)} size="sm" type="button" variant={isSelected ? 'default' : 'ghost'} {...props} > <span className={cn( 'font-medium text-[10px] text-muted-foreground', isSelected && 'text-primary-foreground/70' )} > {month} </span> <span className="font-semibold text-sm">{day}</span> </Button> );};
Installation
npx shadcn@latest add https://www.shadcn.io/registry/mini-calendar.json
npx shadcn@latest add https://www.shadcn.io/registry/mini-calendar.json
pnpm dlx shadcn@latest add https://www.shadcn.io/registry/mini-calendar.json
bunx shadcn@latest add https://www.shadcn.io/registry/mini-calendar.json
Features
- Horizontal layout - Configurable consecutive days display (default: 5) using Tailwind CSS flex
- Navigation controls - Chevron buttons for date range movement using JavaScript event handlers
- Composable design - Separate components for navigation, container, and days using React composition
- Controlled/uncontrolled - Flexible state management with value and onChange props for Next.js applications
- Today highlighting - Accent background for current date with button variant styling using TypeScript
- Accessibility features - Semantic HTML and ARIA attributes using shadcn/ui patterns
- Context management - Component state sharing through React Context for date-fns integration
- Open source - Free mini calendar with Lucide icons and customizable styling
Examples
Basic Usage
'use client';import { MiniCalendar, MiniCalendarDay, MiniCalendarDays, MiniCalendarNavigation,} from '@/components/ui/shadcn-io/mini-calendar';const Example = () => ( <MiniCalendar> <MiniCalendarNavigation direction="prev" /> <MiniCalendarDays> {(date) => <MiniCalendarDay date={date} key={date.toISOString()} />} </MiniCalendarDays> <MiniCalendarNavigation direction="next" /> </MiniCalendar>);export default Example;
'use client';import { useControllableState } from '@radix-ui/react-use-controllable-state';import { addDays, format, isSameDay, isToday } from 'date-fns';import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';import { Slot } from 'radix-ui';import { type ButtonHTMLAttributes, type ComponentProps, createContext, type HTMLAttributes, type MouseEventHandler, type ReactNode, useContext,} from 'react';import { Button } from '@/components/ui/button';import { cn } from '@/lib/utils';// Context for sharing state between componentstype MiniCalendarContextType = { selectedDate: Date | null | undefined; onDateSelect: (date: Date) => void; startDate: Date; onNavigate: (direction: 'prev' | 'next') => void; days: number;};const MiniCalendarContext = createContext<MiniCalendarContextType | null>(null);const useMiniCalendar = () => { const context = useContext(MiniCalendarContext); if (!context) { throw new Error('MiniCalendar components must be used within MiniCalendar'); } return context;};// Helper function to get array of consecutive datesconst getDays = (startDate: Date, count: number): Date[] => { const days: Date[] = []; for (let i = 0; i < count; i++) { days.push(addDays(startDate, i)); } return days;};// Helper function to format dateconst formatDate = (date: Date) => { const month = format(date, 'MMM'); const day = format(date, 'd'); return { month, day };};export type MiniCalendarProps = HTMLAttributes<HTMLDivElement> & { value?: Date; defaultValue?: Date; onValueChange?: (date: Date | undefined) => void; startDate?: Date; defaultStartDate?: Date; onStartDateChange?: (date: Date | undefined) => void; days?: number;};export const MiniCalendar = ({ value, defaultValue, onValueChange, startDate, defaultStartDate = new Date(), onStartDateChange, days = 5, className, children, ...props}: MiniCalendarProps) => { const [selectedDate, setSelectedDate] = useControllableState< Date | undefined >({ prop: value, defaultProp: defaultValue, onChange: onValueChange, }); const [currentStartDate, setCurrentStartDate] = useControllableState({ prop: startDate, defaultProp: defaultStartDate, onChange: onStartDateChange, }); const handleDateSelect = (date: Date) => { setSelectedDate(date); }; const handleNavigate = (direction: 'prev' | 'next') => { const newStartDate = addDays( currentStartDate || new Date(), direction === 'next' ? days : -days ); setCurrentStartDate(newStartDate); }; const contextValue: MiniCalendarContextType = { selectedDate: selectedDate || null, onDateSelect: handleDateSelect, startDate: currentStartDate || new Date(), onNavigate: handleNavigate, days, }; return ( <MiniCalendarContext.Provider value={contextValue}> <div className={cn( 'flex items-center gap-2 rounded-lg border bg-background p-2', className )} {...props} > {children} </div> </MiniCalendarContext.Provider> );};export type MiniCalendarNavigationProps = ButtonHTMLAttributes<HTMLButtonElement> & { direction: 'prev' | 'next'; asChild?: boolean; };export const MiniCalendarNavigation = ({ direction, asChild = false, children, onClick, ...props}: MiniCalendarNavigationProps) => { const { onNavigate } = useMiniCalendar(); const Icon = direction === 'prev' ? ChevronLeftIcon : ChevronRightIcon; const handleClick: MouseEventHandler<HTMLButtonElement> = (event) => { onNavigate(direction); onClick?.(event); }; if (asChild) { return ( <Slot.Root onClick={handleClick} {...props}> {children} </Slot.Root> ); } return ( <Button onClick={handleClick} size={asChild ? undefined : 'icon'} type="button" variant={asChild ? undefined : 'ghost'} {...props} > {children ?? <Icon className="size-4" />} </Button> );};export type MiniCalendarDaysProps = Omit< HTMLAttributes<HTMLDivElement>, 'children'> & { children: (date: Date) => ReactNode;};export const MiniCalendarDays = ({ className, children, ...props}: MiniCalendarDaysProps) => { const { startDate, days: dayCount } = useMiniCalendar(); const days = getDays(startDate, dayCount); return ( <div className={cn('flex items-center gap-1', className)} {...props}> {days.map((date) => children(date))} </div> );};export type MiniCalendarDayProps = ComponentProps<typeof Button> & { date: Date;};export const MiniCalendarDay = ({ date, className, ...props}: MiniCalendarDayProps) => { const { selectedDate, onDateSelect } = useMiniCalendar(); const { month, day } = formatDate(date); const isSelected = selectedDate && isSameDay(date, selectedDate); const isTodayDate = isToday(date); return ( <Button className={cn( 'h-auto min-w-[3rem] flex-col gap-0 p-2 text-xs', isTodayDate && !isSelected && 'bg-accent', className )} onClick={() => onDateSelect(date)} size="sm" type="button" variant={isSelected ? 'default' : 'ghost'} {...props} > <span className={cn( 'font-medium text-[10px] text-muted-foreground', isSelected && 'text-primary-foreground/70' )} > {month} </span> <span className="font-semibold text-sm">{day}</span> </Button> );};
Controlled
'use client';import { MiniCalendar, MiniCalendarDay, MiniCalendarDays, MiniCalendarNavigation,} from '@/components/ui/shadcn-io/mini-calendar';import { useState } from 'react';const Example = () => { const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined); return ( <div className="space-y-4"> <MiniCalendar onValueChange={setSelectedDate} value={selectedDate}> <MiniCalendarNavigation direction="prev" /> <MiniCalendarDays> {(date) => <MiniCalendarDay date={date} key={date.toISOString()} />} </MiniCalendarDays> <MiniCalendarNavigation direction="next" /> </MiniCalendar> {selectedDate && ( <p className="text-muted-foreground text-sm"> Selected:{' '} {selectedDate.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', })} </p> )} </div> );};export default Example;
'use client';import { useControllableState } from '@radix-ui/react-use-controllable-state';import { addDays, format, isSameDay, isToday } from 'date-fns';import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';import { Slot } from 'radix-ui';import { type ButtonHTMLAttributes, type ComponentProps, createContext, type HTMLAttributes, type MouseEventHandler, type ReactNode, useContext,} from 'react';import { Button } from '@/components/ui/button';import { cn } from '@/lib/utils';// Context for sharing state between componentstype MiniCalendarContextType = { selectedDate: Date | null | undefined; onDateSelect: (date: Date) => void; startDate: Date; onNavigate: (direction: 'prev' | 'next') => void; days: number;};const MiniCalendarContext = createContext<MiniCalendarContextType | null>(null);const useMiniCalendar = () => { const context = useContext(MiniCalendarContext); if (!context) { throw new Error('MiniCalendar components must be used within MiniCalendar'); } return context;};// Helper function to get array of consecutive datesconst getDays = (startDate: Date, count: number): Date[] => { const days: Date[] = []; for (let i = 0; i < count; i++) { days.push(addDays(startDate, i)); } return days;};// Helper function to format dateconst formatDate = (date: Date) => { const month = format(date, 'MMM'); const day = format(date, 'd'); return { month, day };};export type MiniCalendarProps = HTMLAttributes<HTMLDivElement> & { value?: Date; defaultValue?: Date; onValueChange?: (date: Date | undefined) => void; startDate?: Date; defaultStartDate?: Date; onStartDateChange?: (date: Date | undefined) => void; days?: number;};export const MiniCalendar = ({ value, defaultValue, onValueChange, startDate, defaultStartDate = new Date(), onStartDateChange, days = 5, className, children, ...props}: MiniCalendarProps) => { const [selectedDate, setSelectedDate] = useControllableState< Date | undefined >({ prop: value, defaultProp: defaultValue, onChange: onValueChange, }); const [currentStartDate, setCurrentStartDate] = useControllableState({ prop: startDate, defaultProp: defaultStartDate, onChange: onStartDateChange, }); const handleDateSelect = (date: Date) => { setSelectedDate(date); }; const handleNavigate = (direction: 'prev' | 'next') => { const newStartDate = addDays( currentStartDate || new Date(), direction === 'next' ? days : -days ); setCurrentStartDate(newStartDate); }; const contextValue: MiniCalendarContextType = { selectedDate: selectedDate || null, onDateSelect: handleDateSelect, startDate: currentStartDate || new Date(), onNavigate: handleNavigate, days, }; return ( <MiniCalendarContext.Provider value={contextValue}> <div className={cn( 'flex items-center gap-2 rounded-lg border bg-background p-2', className )} {...props} > {children} </div> </MiniCalendarContext.Provider> );};export type MiniCalendarNavigationProps = ButtonHTMLAttributes<HTMLButtonElement> & { direction: 'prev' | 'next'; asChild?: boolean; };export const MiniCalendarNavigation = ({ direction, asChild = false, children, onClick, ...props}: MiniCalendarNavigationProps) => { const { onNavigate } = useMiniCalendar(); const Icon = direction === 'prev' ? ChevronLeftIcon : ChevronRightIcon; const handleClick: MouseEventHandler<HTMLButtonElement> = (event) => { onNavigate(direction); onClick?.(event); }; if (asChild) { return ( <Slot.Root onClick={handleClick} {...props}> {children} </Slot.Root> ); } return ( <Button onClick={handleClick} size={asChild ? undefined : 'icon'} type="button" variant={asChild ? undefined : 'ghost'} {...props} > {children ?? <Icon className="size-4" />} </Button> );};export type MiniCalendarDaysProps = Omit< HTMLAttributes<HTMLDivElement>, 'children'> & { children: (date: Date) => ReactNode;};export const MiniCalendarDays = ({ className, children, ...props}: MiniCalendarDaysProps) => { const { startDate, days: dayCount } = useMiniCalendar(); const days = getDays(startDate, dayCount); return ( <div className={cn('flex items-center gap-1', className)} {...props}> {days.map((date) => children(date))} </div> );};export type MiniCalendarDayProps = ComponentProps<typeof Button> & { date: Date;};export const MiniCalendarDay = ({ date, className, ...props}: MiniCalendarDayProps) => { const { selectedDate, onDateSelect } = useMiniCalendar(); const { month, day } = formatDate(date); const isSelected = selectedDate && isSameDay(date, selectedDate); const isTodayDate = isToday(date); return ( <Button className={cn( 'h-auto min-w-[3rem] flex-col gap-0 p-2 text-xs', isTodayDate && !isSelected && 'bg-accent', className )} onClick={() => onDateSelect(date)} size="sm" type="button" variant={isSelected ? 'default' : 'ghost'} {...props} > <span className={cn( 'font-medium text-[10px] text-muted-foreground', isSelected && 'text-primary-foreground/70' )} > {month} </span> <span className="font-semibold text-sm">{day}</span> </Button> );};
Custom Layout
'use client';import { MiniCalendar, MiniCalendarDay, MiniCalendarDays, MiniCalendarNavigation,} from '@/components/ui/shadcn-io/mini-calendar';import { Button } from '@/components/ui/button';import { ArrowLeftIcon, ArrowRightIcon } from 'lucide-react';const Example = () => ( <MiniCalendar className="bg-card p-4 shadow-lg"> <div className="flex items-center gap-4"> <MiniCalendarNavigation asChild direction="prev"> <Button size="icon" variant="outline"> <ArrowLeftIcon className="size-4" /> </Button> </MiniCalendarNavigation> <MiniCalendarDays className="gap-2"> {(date) => <MiniCalendarDay date={date} key={date.toISOString()} />} </MiniCalendarDays> <MiniCalendarNavigation asChild direction="next"> <Button size="icon" variant="outline"> <ArrowRightIcon className="size-4" /> </Button> </MiniCalendarNavigation> </div> </MiniCalendar>);export default Example;
'use client';import { useControllableState } from '@radix-ui/react-use-controllable-state';import { addDays, format, isSameDay, isToday } from 'date-fns';import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';import { Slot } from 'radix-ui';import { type ButtonHTMLAttributes, type ComponentProps, createContext, type HTMLAttributes, type MouseEventHandler, type ReactNode, useContext,} from 'react';import { Button } from '@/components/ui/button';import { cn } from '@/lib/utils';// Context for sharing state between componentstype MiniCalendarContextType = { selectedDate: Date | null | undefined; onDateSelect: (date: Date) => void; startDate: Date; onNavigate: (direction: 'prev' | 'next') => void; days: number;};const MiniCalendarContext = createContext<MiniCalendarContextType | null>(null);const useMiniCalendar = () => { const context = useContext(MiniCalendarContext); if (!context) { throw new Error('MiniCalendar components must be used within MiniCalendar'); } return context;};// Helper function to get array of consecutive datesconst getDays = (startDate: Date, count: number): Date[] => { const days: Date[] = []; for (let i = 0; i < count; i++) { days.push(addDays(startDate, i)); } return days;};// Helper function to format dateconst formatDate = (date: Date) => { const month = format(date, 'MMM'); const day = format(date, 'd'); return { month, day };};export type MiniCalendarProps = HTMLAttributes<HTMLDivElement> & { value?: Date; defaultValue?: Date; onValueChange?: (date: Date | undefined) => void; startDate?: Date; defaultStartDate?: Date; onStartDateChange?: (date: Date | undefined) => void; days?: number;};export const MiniCalendar = ({ value, defaultValue, onValueChange, startDate, defaultStartDate = new Date(), onStartDateChange, days = 5, className, children, ...props}: MiniCalendarProps) => { const [selectedDate, setSelectedDate] = useControllableState< Date | undefined >({ prop: value, defaultProp: defaultValue, onChange: onValueChange, }); const [currentStartDate, setCurrentStartDate] = useControllableState({ prop: startDate, defaultProp: defaultStartDate, onChange: onStartDateChange, }); const handleDateSelect = (date: Date) => { setSelectedDate(date); }; const handleNavigate = (direction: 'prev' | 'next') => { const newStartDate = addDays( currentStartDate || new Date(), direction === 'next' ? days : -days ); setCurrentStartDate(newStartDate); }; const contextValue: MiniCalendarContextType = { selectedDate: selectedDate || null, onDateSelect: handleDateSelect, startDate: currentStartDate || new Date(), onNavigate: handleNavigate, days, }; return ( <MiniCalendarContext.Provider value={contextValue}> <div className={cn( 'flex items-center gap-2 rounded-lg border bg-background p-2', className )} {...props} > {children} </div> </MiniCalendarContext.Provider> );};export type MiniCalendarNavigationProps = ButtonHTMLAttributes<HTMLButtonElement> & { direction: 'prev' | 'next'; asChild?: boolean; };export const MiniCalendarNavigation = ({ direction, asChild = false, children, onClick, ...props}: MiniCalendarNavigationProps) => { const { onNavigate } = useMiniCalendar(); const Icon = direction === 'prev' ? ChevronLeftIcon : ChevronRightIcon; const handleClick: MouseEventHandler<HTMLButtonElement> = (event) => { onNavigate(direction); onClick?.(event); }; if (asChild) { return ( <Slot.Root onClick={handleClick} {...props}> {children} </Slot.Root> ); } return ( <Button onClick={handleClick} size={asChild ? undefined : 'icon'} type="button" variant={asChild ? undefined : 'ghost'} {...props} > {children ?? <Icon className="size-4" />} </Button> );};export type MiniCalendarDaysProps = Omit< HTMLAttributes<HTMLDivElement>, 'children'> & { children: (date: Date) => ReactNode;};export const MiniCalendarDays = ({ className, children, ...props}: MiniCalendarDaysProps) => { const { startDate, days: dayCount } = useMiniCalendar(); const days = getDays(startDate, dayCount); return ( <div className={cn('flex items-center gap-1', className)} {...props}> {days.map((date) => children(date))} </div> );};export type MiniCalendarDayProps = ComponentProps<typeof Button> & { date: Date;};export const MiniCalendarDay = ({ date, className, ...props}: MiniCalendarDayProps) => { const { selectedDate, onDateSelect } = useMiniCalendar(); const { month, day } = formatDate(date); const isSelected = selectedDate && isSameDay(date, selectedDate); const isTodayDate = isToday(date); return ( <Button className={cn( 'h-auto min-w-[3rem] flex-col gap-0 p-2 text-xs', isTodayDate && !isSelected && 'bg-accent', className )} onClick={() => onDateSelect(date)} size="sm" type="button" variant={isSelected ? 'default' : 'ghost'} {...props} > <span className={cn( 'font-medium text-[10px] text-muted-foreground', isSelected && 'text-primary-foreground/70' )} > {month} </span> <span className="font-semibold text-sm">{day}</span> </Button> );};
Custom Days
'use client';import { MiniCalendar, MiniCalendarDay, MiniCalendarDays, MiniCalendarNavigation,} from '@/components/ui/shadcn-io/mini-calendar';const Example = () => ( <MiniCalendar days={7}> <MiniCalendarNavigation direction="prev" /> <MiniCalendarDays> {(date) => <MiniCalendarDay date={date} key={date.toISOString()} />} </MiniCalendarDays> <MiniCalendarNavigation direction="next" /> </MiniCalendar>);export default Example;
'use client';import { useControllableState } from '@radix-ui/react-use-controllable-state';import { addDays, format, isSameDay, isToday } from 'date-fns';import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';import { Slot } from 'radix-ui';import { type ButtonHTMLAttributes, type ComponentProps, createContext, type HTMLAttributes, type MouseEventHandler, type ReactNode, useContext,} from 'react';import { Button } from '@/components/ui/button';import { cn } from '@/lib/utils';// Context for sharing state between componentstype MiniCalendarContextType = { selectedDate: Date | null | undefined; onDateSelect: (date: Date) => void; startDate: Date; onNavigate: (direction: 'prev' | 'next') => void; days: number;};const MiniCalendarContext = createContext<MiniCalendarContextType | null>(null);const useMiniCalendar = () => { const context = useContext(MiniCalendarContext); if (!context) { throw new Error('MiniCalendar components must be used within MiniCalendar'); } return context;};// Helper function to get array of consecutive datesconst getDays = (startDate: Date, count: number): Date[] => { const days: Date[] = []; for (let i = 0; i < count; i++) { days.push(addDays(startDate, i)); } return days;};// Helper function to format dateconst formatDate = (date: Date) => { const month = format(date, 'MMM'); const day = format(date, 'd'); return { month, day };};export type MiniCalendarProps = HTMLAttributes<HTMLDivElement> & { value?: Date; defaultValue?: Date; onValueChange?: (date: Date | undefined) => void; startDate?: Date; defaultStartDate?: Date; onStartDateChange?: (date: Date | undefined) => void; days?: number;};export const MiniCalendar = ({ value, defaultValue, onValueChange, startDate, defaultStartDate = new Date(), onStartDateChange, days = 5, className, children, ...props}: MiniCalendarProps) => { const [selectedDate, setSelectedDate] = useControllableState< Date | undefined >({ prop: value, defaultProp: defaultValue, onChange: onValueChange, }); const [currentStartDate, setCurrentStartDate] = useControllableState({ prop: startDate, defaultProp: defaultStartDate, onChange: onStartDateChange, }); const handleDateSelect = (date: Date) => { setSelectedDate(date); }; const handleNavigate = (direction: 'prev' | 'next') => { const newStartDate = addDays( currentStartDate || new Date(), direction === 'next' ? days : -days ); setCurrentStartDate(newStartDate); }; const contextValue: MiniCalendarContextType = { selectedDate: selectedDate || null, onDateSelect: handleDateSelect, startDate: currentStartDate || new Date(), onNavigate: handleNavigate, days, }; return ( <MiniCalendarContext.Provider value={contextValue}> <div className={cn( 'flex items-center gap-2 rounded-lg border bg-background p-2', className )} {...props} > {children} </div> </MiniCalendarContext.Provider> );};export type MiniCalendarNavigationProps = ButtonHTMLAttributes<HTMLButtonElement> & { direction: 'prev' | 'next'; asChild?: boolean; };export const MiniCalendarNavigation = ({ direction, asChild = false, children, onClick, ...props}: MiniCalendarNavigationProps) => { const { onNavigate } = useMiniCalendar(); const Icon = direction === 'prev' ? ChevronLeftIcon : ChevronRightIcon; const handleClick: MouseEventHandler<HTMLButtonElement> = (event) => { onNavigate(direction); onClick?.(event); }; if (asChild) { return ( <Slot.Root onClick={handleClick} {...props}> {children} </Slot.Root> ); } return ( <Button onClick={handleClick} size={asChild ? undefined : 'icon'} type="button" variant={asChild ? undefined : 'ghost'} {...props} > {children ?? <Icon className="size-4" />} </Button> );};export type MiniCalendarDaysProps = Omit< HTMLAttributes<HTMLDivElement>, 'children'> & { children: (date: Date) => ReactNode;};export const MiniCalendarDays = ({ className, children, ...props}: MiniCalendarDaysProps) => { const { startDate, days: dayCount } = useMiniCalendar(); const days = getDays(startDate, dayCount); return ( <div className={cn('flex items-center gap-1', className)} {...props}> {days.map((date) => children(date))} </div> );};export type MiniCalendarDayProps = ComponentProps<typeof Button> & { date: Date;};export const MiniCalendarDay = ({ date, className, ...props}: MiniCalendarDayProps) => { const { selectedDate, onDateSelect } = useMiniCalendar(); const { month, day } = formatDate(date); const isSelected = selectedDate && isSameDay(date, selectedDate); const isTodayDate = isToday(date); return ( <Button className={cn( 'h-auto min-w-[3rem] flex-col gap-0 p-2 text-xs', isTodayDate && !isSelected && 'bg-accent', className )} onClick={() => onDateSelect(date)} size="sm" type="button" variant={isSelected ? 'default' : 'ghost'} {...props} > <span className={cn( 'font-medium text-[10px] text-muted-foreground', isSelected && 'text-primary-foreground/70' )} > {month} </span> <span className="font-semibold text-sm">{day}</span> </Button> );};
Use Cases
- Quick date selection - Recent date picking for forms and scheduling
- Dashboard widgets - Compact calendar controls in admin interfaces
- Event creation - Near-term date selection for appointments and tasks
- Timeline navigation - Date-based content filtering and browsing
Implementation
Built with date-fns for date calculations. Uses React Context for component communication. Supports controlled and uncontrolled modes. Lucide icons for navigation with semantic HTML.
Calendar
Feature calendar grid component for React and Next.js applications. Built with TypeScript support, Tailwind CSS styling, and shadcn/ui design featuring drag-and-drop, date selection, and internationalization.
Relative Time
Multi-timezone time display component for React and Next.js applications. Built with TypeScript support, Tailwind CSS styling, and shadcn/ui design featuring auto-updates, custom formatting, and controlled time states.