1. Components
  2. Date and time
  3. Range Calendar

Range Calendar

A component that allows users to select a range of dates.

Trip dates, April 2025

30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1
2
3
<RangeCalendar aria-label="Trip dates" />

Installation

npx dotui-cli@latest add calendar

Usage

Use RangeCalendar to allow users to select a contiguous or non-contiguous range of dates.

import { parseDate } from "@internationalized/date";
import { RangeCalendar } from "@/components/core/calendar";
 
<RangeCalendar
  aria-label="Trip dates"
  defaultValue={{
    start: parseDate("2020-02-03"),
    end: parseDate("2020-02-12"),
  }}
/>;
import { parseDate } from "@internationalized/date";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { Button } from "@/components/core/button";
import {
  RangeCalendarRoot,
  CalendarHeader,
  CalendarGrid,
  CalendarGridHeader,
  CalendarHeaderCell,
  CalendarGridBody,
  CalendarCell,
} from "@/components/core/calendar";
import { Heading } from "@/registry/ui/default/core/heading";
 
<RangeCalendarRoot
  aria-label="Trip dates"
  defaultValue={{
    start: parseDate("2020-02-03"),
    end: parseDate("2020-02-12"),
  }}
>
  <CalendarHeader>
    <Button slot="previous">
      <ChevronLeftIcon />
    </Button>
    <Heading />
    <Button slot="next">
      <ChevronRightIcon />
    </Button>
  </CalendarHeader>
  <CalendarGrid>
    <CalendarGridHeader>
      {(day) => <CalendarHeaderCell>{day}</CalendarHeaderCell>}
    </CalendarGridHeader>
    <CalendarGridBody>
      {(date) => <CalendarCell date={date} />}
    </CalendarGridBody>
  </CalendarGrid>
</RangeCalendarRoot>;

Best practices

  • An aria-label must be provided to the RangeCalendar for accessibility. If it is labeled by a separate element, an aria-labelledby prop must be provided using the id of the labeling element instead.

Uncontrolled

An initial, uncontrolled value can be provided to the RangeCalendar using the defaultValue prop.

Trip dates, February 2020

26
27
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<RangeCalendar
  aria-label="Trip dates"
  defaultValue={{
    start: parseDate("2020-02-03"),
    end: parseDate("2020-02-12"),
  }}
/>

Controlled

The Range Calendar component can be controlled by passing the value and onChange props.

Trip dates, February 2020

26
27
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

Start date: 2020-02-03
End date: 2020-02-12

const [value, setValue] = React.useState<DateRange>({
  start: parseDate("2020-02-03"),
  end: parseDate("2020-02-12"),
});
return <RangeCalendar aria-label="Trip dates" value={value} onChange={setValue} />

Validation

Min and max values

By default, Calendar allows selecting any date. The minValue and maxValue props can also be used to prevent the user from selecting dates outside a certain range.

This example only accepts dates after today.

Trip dates, April 2025

30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1
2
3
<RangeCalendar aria-label="Trip dates" minValue={today(getLocalTimeZone())} />

Unavailable dates

Calendar supports marking certain dates as unavailable. These dates cannot be selected by the user and are displayed with a crossed out appearance. The isDateUnavailable prop accepts a callback that is called to evaluate whether each visible date is unavailable.

This example includes multiple unavailable date ranges, e.g. dates when a rental house is not available. The minValue prop is also used to prevent selecting dates before today.

Trip dates, April 2025

30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1
2
3
const now = today(getLocalTimeZone());
const disabledRanges = [
  [now, now.add({ days: 5 })],
  [now.add({ days: 14 }), now.add({ days: 16 })],
  [now.add({ days: 23 }), now.add({ days: 24 })],
];

const isDateUnavailable = (date: DateValue) =>
  disabledRanges.some(
    (interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0
  );

return (
  <RangeCalendar
    aria-label="Trip dates"
    minValue={today(getLocalTimeZone())}
    isDateUnavailable={isDateUnavailable}
  />
);

Non-contiguous ranges

The allowsNonContiguousRanges prop enables a range to be selected even if there are unavailable dates in the middle.

This example prevents selecting weekends, but allows selecting ranges that span multiple weeks.

Trip dates, April 2025

30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1
2
3
const now = today(getLocalTimeZone());
const disabledRanges = [
  [now, now.add({ days: 5 })],
  [now.add({ days: 14 }), now.add({ days: 16 })],
  [now.add({ days: 23 }), now.add({ days: 24 })],
];
const isDateUnavailable = (date: DateValue) =>
  disabledRanges.some(
    (interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0
  );
return (
  <RangeCalendar
    aria-label="Trip dates"
    minValue={today(getLocalTimeZone())}
    isDateUnavailable={isDateUnavailable}
    allowsNonContiguousRanges
  />
);

Error message

RangeCalendar tries to avoid allowing the user to select invalid dates in the first place (see Min and max values and Unavailable dates). However, if according to application logic a selected date is invalid, Use isInvalid and errorMessage props.

This example validates that the selected date range is a maximum of 1 week in duration.

Trip dates, April 2025

30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1
2
3
Maximum stay duration is 1 week
const [range, setRange] = React.useState({
  start: today(getLocalTimeZone()),
  end: today(getLocalTimeZone()).add({ weeks: 1, days: 3 }),
});
const isInvalid = range.end.compare(range.start) > 7;
return (
  <RangeCalendar
    aria-label="Trip dates"
    value={range}
    onChange={setRange}
    isInvalid={isInvalid}
    errorMessage={isInvalid ? "Maximum stay duration is 1 week" : undefined}
  />
);

Options

Visible months

By default, the RangeCalendar displays a single month. The visibleMonths prop allows displaying up to 3 months at a time.

Trip dates, April to May 2025

30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1
2
3
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<RangeCalendar aria-label="Trip dates" visibleMonths={2} />

Page behaviour

The pageBehavior prop allows you to control how the range-calendar navigates between months.

Trip dates, April to May 2025

30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1
2
3
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<RangeCalendar aria-label="Trip dates" visibleMonths={2} pageBehavior="single" />

Disabled

The isDisabled boolean prop makes the RangeCalendar disabled. cells cannot be focused or selected.

Trip dates, April 2025

30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1
2
3
<RangeCalendar aria-label="Trip dates" isDisabled />

Read only

The isReadOnly boolean prop makes the RangeCalendar's value immutable. Unlike isDisabled, the RangeCalendar remains focusable.

Trip dates, February 2020

26
27
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<RangeCalendar
  aria-label="Trip dates"
  isReadOnly
  value={{
    start: parseDate("2020-02-03"),
    end: parseDate("2020-02-12"),
  }}
/>

Composition

If you need to customize things further, you can drop down to the composition level.

Trip dates, April 2025

30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1
2
3
<RangeCalendarRoot aria-label="Trip dates">
  <CalendarHeader>
    <Button slot="previous">
      <ChevronLeftIcon />
    </Button>
    <Heading />
    <Button slot="next">
      <ChevronRightIcon />
    </Button>
  </CalendarHeader>
  <CalendarGrid>
    <CalendarGridHeader>
      {(day) => <CalendarHeaderCell>{day}</CalendarHeaderCell>}
    </CalendarGridHeader>
    <CalendarGridBody>
      {(date) => <CalendarCell date={date} />}
    </CalendarGridBody>
  </CalendarGrid>
</RangeCalendarRoot>

API Reference

PropTypeDefaultDescription
visibleMonthsnumber1The number of months to display at once. Up to 3 months are supported.
minValueDateValue-The minimum allowed date that a user may select.
maxValueDateValue-The maximum allowed date that a user may select.
isDateUnavailable(date: DateValue) => boolean-Callback that is called for each date of the range calendar. If it returns true, then the date is unavailable.
isDisabledbooleanfalseWhether the range calendar is disabled.
isReadOnlybooleanfalseWhether the range calendar value is immutable.
autoFocusbooleanfalseWhether to automatically focus the range calendar when it mounts.
focusedValueDateValue-Controls the currently focused date within the range calendar.
defaultFocusedValueDateValue-The date that is focused when the range calendar first mounts (uncountrolled).
isInvalidboolean-Whether the current selection is invalid according to application logic.
pageBehavior'single' | 'visible''visible'Controls the behavior of paging. Pagination either works by advancing the visible page by visibleDuration (default) or one unit of visibleDuration.
valueRangeValue<DateValue> | null-The current value (controlled).
defaultValueRangeValue<DateValue> | null-The default value (uncontrolled).
childrenReactNode | (values: RangeCalendarRenderProps & {defaultChildren: ReactNode | undefined}) => ReactNode-The children of the component. A function may be provided to alter the children based on component state.
classNamestring-The CSS className for the element.
styleCSSProperties | (values: RangeCalendarRenderProps & {defaultStyle: CSSProperties}) => CSSProperties-The inline style for the element. A function may be provided to compute the style based on component state.
EventTypeDescription
onFocusChange(date: CalendarDate) => voidHandler that is called when the focused date changes.
onChange(value: RangeValue<MappedDateValue<DateValue>>) => voidHandler that is called when the value changes.

Accessibility

Keyboard interactions

KeyDescription
TabMoves focus to the next focusable item in the range calendar.
Shift+TabMoves focus to the previous focusable item in the range calendar.
ArrowRightMoves focus to the next day.
ArrowLeftMoves focus to the previous day.
ArrowDownMoves focus to the same day of the week in the next week.
ArrowUpMoves focus to the same day of the week in the previous week.
Space EnterSelects the focused date.

Built by mehdibha. The source code is available on GitHub.