Beta Preview

RangeCalendar

A range calendar displays one or more date grids and allows users to select a contiguous range of dates.

October 2025

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
1
isDisabled 
Example
RangeCalendar.tsx
RangeCalendar.css
import {RangeCalendar} from './RangeCalendar';

<RangeCalendar />

Value

Use the value or defaultValue prop to set the selected date range, using objects in the @internationalized/date package. This library supports parsing date strings in multiple formats, manipulation across international calendar systems, time zones, etc.

February 2025

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
1

Selected range: February 3 – 12, 2025

import {parseDate, getLocalTimeZone} from '@internationalized/date';
import {useDateFormatter} from 'react-aria';
import {RangeCalendar} from './RangeCalendar';
import {useState} from 'react';

function Example() {
  let [range, setRange] = useState({
    start: parseDate('2025-02-03'),
    end: parseDate('2025-02-12')
  });
  let formatter = useDateFormatter({ dateStyle: 'long' });
  
  return (
    <>
      <RangeCalendar
        value={range}
        onChange={setRange}
      />
      <p>Selected range: {formatter.formatRange(
        range.start.toDate(getLocalTimeZone()),
        range.end.toDate(getLocalTimeZone())
      )}</p>
    </>
  );
}

International calendars

By default, RangeCalendar displays the value using the calendar system for the user's locale. Use <I18nProvider> to override the calendar system by setting the Unicode calendar locale extension. The onChange event always receives a date in the same calendar as the value or defaultValue (Gregorian if no value is provided), regardless of the displayed locale.

शक 1946 माघ

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
1
2
3
Locale
Calendar
import {I18nProvider} from 'react-aria-components';
import {parseDate} from '@internationalized/date';
import {RangeCalendar} from './RangeCalendar';

<I18nProvider locale="hi-IN-u-ca-indian">
  <RangeCalendar
    defaultValue={{
      start: parseDate('2025-02-03'),
      end: parseDate('2025-02-12')
    }} />
</I18nProvider>

Custom calendar systems

RangeCalendar also supports custom calendar systems that implement custom business rules, for example a fiscal year calendar that follows a 4-5-4 format, where month ranges don't follow the usual Gregorian calendar. See the @internationalized/date docs for an example implementation.

October 2025

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
import type {AnyCalendarDate} from '@internationalized/date';
import {CalendarDate, startOfWeek, toCalendar, GregorianCalendar} from '@internationalized/date';
import {RangeCalendar} from './RangeCalendar';

export default (
  <RangeCalendar
    firstDayOfWeek="sun"
    createCalendar={() => new Custom454()} />
);

// See @internationalized/date docs linked above.

Validation

Use the minValue and maxValue props to set the valid date range. The isDateUnavailable callback prevents certain dates from being selected. Use allowsNonContiguousRanges to allow selecting ranges containing unavailable dates. For custom validation rules, set the isInvalid prop and the errorMessage slot.

Trip dates, October 2025

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
1
Maximum stay duration is 1 week
allowsNonContiguousRanges 
import {today, getLocalTimeZone} from '@internationalized/date';
import {useLocale} from 'react-aria';
import {RangeCalendar} from './RangeCalendar';
import {useState} from 'react';

function Example(props) {
  let {locale} = useLocale();
  let now = today(getLocalTimeZone());
  let [range, setRange] = useState({
    start: now.add({days: 6}),
    end: now.add({ days: 14 })
  });
  let disabledRanges = [
    [now, now.add({ days: 5 })],
    [now.add({ days: 15 }), now.add({ days: 17 })],
    [now.add({ days: 23 }), now.add({ days: 24 })]
  ];
  let isInvalid = range.end.compare(range.start) > 7;

  return (
    <RangeCalendar
      {...props}
      aria-label="Trip dates"
      value={range}
      onChange={setRange}
      minValue={today(getLocalTimeZone())}
      isDateUnavailable={date => (
        disabledRanges.some((interval) =>
          date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0
        )
      )}
      isInvalid={isInvalid}
      errorMessage={isInvalid ? 'Maximum stay duration is 1 week' : undefined} />
  );
}

Display options

Set the visibleDuration prop and render multiple CalendarGrid elements to display more than one month at a time. The pageBehavior prop controls whether pagination advances by a single month or multiple. The firstDayOfWeek prop overrides the locale-specified first day of the week.

Trip dates, October to November 2025

October 2025

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
1

November 2025

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
30
1
2
3
4
5
6
 
pageBehavior 
firstDayOfWeek 
import {RangeCalendar, Heading, Button, CalendarGrid, CalendarCell} from 'react-aria-components';
import {useDateFormatter} from 'react-aria';

// TODO: move this into the starter example.
function Example(props) {
  let monthFormatter = useDateFormatter({
    month: 'long',
    year: 'numeric',
  });

  return (
    <RangeCalendar
      {...props}
      aria-label="Trip dates"
      visibleDuration={{months: 2}}
      style={{display: 'flex', gap: 30, overflow: 'auto'}}
    >
      {({state}) => (
        [...Array(props.visibleDuration.months).keys()].map(i => (
          <div key={i} style={{flex: 1}}>
            <header>
              {i === 0 && 
                <Button slot="previous">◀</Button>
              }
              <h2 style={{flex: 1, textAlign: 'center'}}>{monthFormatter.format(state.visibleRange.start.add({months: i}).toDate(state.timeZone))}</h2>
              {i === props.visibleDuration.months - 1 && 
                <Button slot="next">▶</Button>
              }
            </header>
            <CalendarGrid offset={{months: i}}>
              {date => <CalendarCell date={date} />}
            </CalendarGrid>
          </div>
        ))
      )}
    </RangeCalendar>
  );
}

Controlling the focused date

Use the focusedValue or defaultFocusedValue prop to control which date is focused. This controls which month is visible. The onFocusChange event is called when a date is focused by the user.

July 2021

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
import {RangeCalendar} from './RangeCalendar';
import {Button} from './Button';
import {CalendarDate, today, getLocalTimeZone} from '@internationalized/date';
import {useState} from 'react';

function Example() {
  let defaultDate = new CalendarDate(2021, 7, 1);
  let [focusedDate, setFocusedDate] = useState(defaultDate);

  return (
    <div>
      <Button
        style={{marginBottom: 20}}
        onPress={() => setFocusedDate(today(getLocalTimeZone()))}>
        Today
      </Button>
      <RangeCalendar
        focusedValue={focusedDate}
        onFocusChange={setFocusedDate}
      />
    </div>
  );
}

Month and year pickers

You can also control the focused date via CalendarStateContext. This example shows month and year dropdown components that work inside any <RangeCalendar>.

October 2025

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
1
Example
MonthDropdown.tsx
YearDropdown.tsx
import {RangeCalendar, CalendarGrid, CalendarCell} from 'react-aria-components';
import {MonthDropdown} from './MonthDropdown';
import {YearDropdown} from './YearDropdown';
import {Button} from './Button';

<RangeCalendar>
  <header style={{display: 'flex', gap: 4}}>
    <Button slot="previous">◀</Button>
    <MonthDropdown />
    <YearDropdown />
    <Button slot="next">▶</Button>
  </header>
  <CalendarGrid>
    {(date) => <CalendarCell date={date} />}
  </CalendarGrid>
</RangeCalendar>

API

September 2021SMTWTF5789101314151619202122232412326272930628S111825412176CellGridNext buttonPrevious button