RangeCalendar
A range calendar displays one or more date grids and allows users to select a contiguous range of dates.
Theme
Indigo Blue Cyan Turquoise Green Yellow Orange Red Pink Purple
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
RangeCalendar.tsx
RangeCalendar.css
Example
RangeCalendar.tsx
RangeCalendar.css
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 >
</>
);
}
Expand code
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
Amharic (Ethiopia) Arabic (Algeria) Arabic (Egypt) Arabic (Saudi Arabia) Arabic (United Arab Emirates) Bulgarian (Bulgaria) Chinese (China) Chinese (Taiwan) Croatian (Croatia) Czech (Czechia) Danish (Denmark) Dutch (Netherlands) English (United Kingdom) English (United States) Estonian (Estonia) Finnish (Finland) French (Canada) French (France) German (Germany) Greek (Greece) Hebrew (Israel) Hindi (India) Hungarian (Hungary) Italian (Italy) Japanese (Japan) Korean (South Korea) Latvian (Latvia) Lithuanian (Lithuania) Norwegian Bokmål (Norway) Persian (Afghanistan) Polish (Poland) Portuguese (Brazil) Romanian (Romania) Russian (Russia) Serbian (SP) Slovak (Slovakia) Slovenian (Slovenia) Spanish (Spain) Swedish (Sweden) Thai (Thailand) Turkish (Türkiye) Ukrainian (Ukraine)
Calendar
Gregorian Indian Japanese Buddhist Taiwan Persian Islamic (Umm al-Qura) Islamic Civil Islamic Tabular Hebrew Coptic Ethiopic Ethiopic (Amete Alem)
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 >
Expand code
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.
Custom454 GregorianCalendar {} weekPattern = [4 , 5 , 4 , 4 , 5 , 4 , 4 , 5 , 4 , 4 , 5 , 4 ];
getDaysInMonth (date ) {
return this .weekPattern [date .month - 1 ] * 7 ;
}
fromJulianDay (jd : number): CalendarDate {
let gregorian = super .fromJulianDay (jd );
let monthStart = startOfWeek (new CalendarDate(gregorian .year , 1 , 1 ), 'en' );
for (let months = 0 ; months < this .weekPattern .length ; months ++) {
let weeksInMonth = this .weekPattern [months ];
let monthEnd = monthStart .add ({weeks : weeksInMonth });
if (monthEnd .compare (gregorian ) > 0 ) {
let days = gregorian .compare (monthStart );
return new CalendarDate(this , monthStart .year , months + 1 , days + 1 );
}
monthStart = monthEnd ;
}
throw Error('Date is not in any month somehow!' );
}
toJulianDay (date : AnyCalendarDate): number {
let monthStart = startOfWeek (new CalendarDate(date .year , 1 , 1 ), 'en' );
for (let month = 1 ; month < date .month ; month ++) {
monthStart = monthStart .add ({weeks : this .weekPattern [month - 1 ]});
}
let gregorian = monthStart .add ({days : date .day - 1 });
return super .toJulianDay (gregorian );
}
getFormattableMonth (date ) {
let gregorian = toCalendar (date , new GregorianCalendar());
return gregorian .set ({month : date .month , day : 1 });
}
isEqual (other ) {
return other instanceof Custom454;
}
}
Expand code
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
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} />
);
}
Expand code
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 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
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 single visible
firstDayOfWeek The day that starts the week.
'sun'
| 'mon'
| 'tue'
| 'wed'
| 'thu'
| 'fri'
| 'sat'
Default sun mon tue wed thu fri sat
import {RangeCalendar, Heading} from 'react-aria-components' ;
import {CalendarGrid, CalendarCell} from './RangeCalendar' ;
import {Button} from './Button' ;
import {useDateFormatter } from 'react-aria' ;
import {ChevronLeft, ChevronRight} from 'lucide-react' ;
// 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 : 12 , overflow : 'auto' }}
>
{({state}) => (
[...Array(props .visibleDuration .months ).keys ()].map (i => (
<div key ={i } style ={{flex : 1 }}>
<header style ={{minHeight : 32 }}>
{i === 0 &&
<Button slot ="previous" variant ="quiet" >
<ChevronLeft />
</Button >
}
<Heading >{monthFormatter .format (state .visibleRange .start .add ({months : i }).toDate (state .timeZone ))}</Heading >
{i === props .visibleDuration .months - 1 &&
<Button slot ="next" variant ="quiet" >
<ChevronRight />
</Button >
}
</header >
<CalendarGrid offset ={{months : i }}>
{date => <CalendarCell date ={date } />}
</CalendarGrid >
</div >
))
)}
</RangeCalendar >
);
}
Expand code
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.
Today
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 >
);
}
Expand code
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
Example
MonthDropdown.tsx
YearDropdown.tsx
MonthDropdown.tsx
YearDropdown.tsx
import {RangeCalendar} from 'react-aria-components' ;
import {CalendarGrid, CalendarCell} from './RangeCalendar' ;
import {MonthDropdown} from './MonthDropdown' ;
import {YearDropdown} from './YearDropdown' ;
import {Button} from './Button' ;
import {ChevronLeft, ChevronRight} from 'lucide-react' ;
<RangeCalendar >
<header style ={{display : 'flex' , gap : 4 }}>
<Button slot ="previous" variant ="quiet" >
<ChevronLeft />
</Button >
<MonthDropdown />
<YearDropdown />
<Button slot ="next" variant ="quiet" >
<ChevronRight />
</Button >
</header >
<CalendarGrid >
{(date ) => <CalendarCell date ={date } />}
</CalendarGrid >
</RangeCalendar >
Expand code
API
September 2021 S M T W T F 5 7 8 9 10 13 14 15 16 19 20 21 22 23 24 1 2 3 26 27 29 30 6 28 S 11 18 25 4 12 17 6 Cell Grid Next button Previous button