UIPackage

Range Calendar

React date-time
Edit on GitHub

Calendar variant for from/to date selection — click two dates and the range fills in between. Same min/max and disabled-date support as the single-date Calendar.

Also available for Vue ->

Installation

$ npx shadcn@latest add https://react.uipkge.dev/r/react/range-calendar.json

Or with the named registry: npx shadcn@latest add @uipkge-react/range-calendar

Examples

Props

Name Type / Values Default Required
selected DateRange optional
onSelect (range: DateRange | undefined) => void optional

Dependencies

Used by

Files (2)

  • components/ui/range-calendar/RangeCalendar.tsx 4.4 kB
    'use client'
    
    import * as React from 'react'
    import { ChevronLeft, ChevronRight } from 'lucide-react'
    import { DayPicker, type DateRange } from 'react-day-picker'
    import { cn } from '@/lib/utils'
    import { buttonVariants } from '@/components/ui/button'
    
    // `selected`/`onSelect` are re-declared explicitly: a bare
    // Omit<ComponentProps<DayPicker>, 'mode'> distributes over DayPicker's prop
    // union and drops `selected` (not common to every mode), so consumers couldn't
    // pass a range. Forcing mode="range" makes the range shape exact.
    export interface RangeCalendarProps
      extends Omit<React.ComponentProps<typeof DayPicker>, 'mode' | 'selected' | 'onSelect'> {
      selected?: DateRange
      onSelect?: (range: DateRange | undefined) => void
    }
    
    /**
     * Date-range calendar — click two dates and the range fills in between. The
     * React mirror of the reka-ui-based Vue RangeCalendar: range start/end cells
     * get the primary fill, the middle gets the accent fill, with the same cell
     * sizes, today / outside / disabled states, and outline nav chevrons mapped
     * 1:1 from the Vue registry's Tailwind class strings. Forces `mode="range"`.
     */
    function RangeCalendar({ className, classNames, showOutsideDays = true, selected, onSelect, ...props }: RangeCalendarProps) {
      return (
        <DayPicker
          mode="range"
          selected={selected}
          onSelect={onSelect}
          data-uipkge=""
          data-slot="range-calendar"
          showOutsideDays={showOutsideDays}
          className={cn('p-3', className)}
          classNames={{
            months: 'mt-4 flex flex-col gap-y-4 sm:flex-row sm:gap-x-4 sm:gap-y-0',
            month: 'flex flex-col gap-4',
            month_caption: 'relative flex h-9 items-center justify-center',
            caption_label: 'text-sm font-medium',
            nav: 'absolute inset-x-0 top-0 flex items-center justify-between gap-1',
            button_previous: cn(
              buttonVariants({ variant: 'outline' }),
              'size-9 bg-transparent p-0 opacity-70 hover:opacity-100 focus-visible:opacity-100',
            ),
            button_next: cn(
              buttonVariants({ variant: 'outline' }),
              'size-9 bg-transparent p-0 opacity-70 hover:opacity-100 focus-visible:opacity-100',
            ),
            month_grid: 'w-full border-collapse space-y-1',
            weekdays: 'flex',
            weekday: 'text-muted-foreground flex-1 rounded-md text-xs font-normal',
            week: 'mt-2 flex w-full',
            day: cn(
              'relative flex-1 p-0 text-center text-sm focus-within:relative focus-within:z-20',
              '[&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md',
              '[&:has([aria-selected].day-outside)]:bg-accent/50 first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md',
            ),
            day_button: cn(
              buttonVariants({ variant: 'ghost' }),
              'h-8 w-8 cursor-pointer p-0 font-normal aria-selected:opacity-100',
            ),
            today: '[&:not([aria-selected])]:bg-accent [&:not([aria-selected])]:text-accent-foreground',
            outside: 'day-outside text-muted-foreground aria-selected:text-muted-foreground',
            disabled: 'text-muted-foreground opacity-50',
            // Range start / end → primary fill (mirrors Vue's data-[selection-start] / -end)
            range_start:
              'day-range-start rounded-l-md [&>button]:bg-primary [&>button]:text-primary-foreground [&>button:hover]:bg-primary [&>button:hover]:text-primary-foreground [&>button:focus]:bg-primary [&>button:focus]:text-primary-foreground',
            range_end:
              'day-range-end rounded-r-md [&>button]:bg-primary [&>button]:text-primary-foreground [&>button:hover]:bg-primary [&>button:hover]:text-primary-foreground [&>button:focus]:bg-primary [&>button:focus]:text-primary-foreground',
            // Range middle → accent fill
            range_middle: 'aria-selected:bg-accent aria-selected:text-accent-foreground [&>button]:hover:bg-accent',
            hidden: 'invisible',
            ...classNames,
          }}
          components={{
            Chevron: ({ orientation, className: chevronClassName, ...chevronProps }) =>
              orientation === 'left' ? (
                <ChevronLeft className={cn('size-4', chevronClassName)} aria-hidden="true" {...chevronProps} />
              ) : (
                <ChevronRight className={cn('size-4', chevronClassName)} aria-hidden="true" {...chevronProps} />
              ),
          }}
          {...props}
        />
      )
    }
    RangeCalendar.displayName = 'RangeCalendar'
    
    export { RangeCalendar }
  • components/ui/range-calendar/index.ts 0.1 kB
    export { RangeCalendar, type RangeCalendarProps } from './RangeCalendar'

Raw manifest: https://react.uipkge.dev/r/react/range-calendar.json