@@ -4,12 +4,6 @@ sidebar_position: 2
44
55# Custom Selections
66
7- :::info Draft
8-
9- This documentation is still a work in progress. If you have any questions, please visit the [ discussions] ( https://github.com/gpbl/react-day-picker/discussions ) page on GitHub.
10-
11- :::
12-
137If the built-in [ selection modes] ( ../selections/selection-modes.mdx ) do not satisfy your app’s requirements, you can control the selection behavior using the [ ` onDayClick ` ] ( ../api/interfaces/PropsBase.md#ondayclick ) and [ ` modifiers ` ] ( ../api/type-aliases/Modifiers.md ) props.
148
159| Prop | Type | Description |
@@ -19,6 +13,13 @@ If the built-in [selection modes](../selections/selection-modes.mdx) do not sati
1913
2014The ` onDayClick ` prop is a function that receives the clicked day and its modifiers as arguments.
2115
16+ ## How custom selection works
17+
18+ - Use ` mode="default" ` (or omit ` mode ` ) to disable built-in selection logic.
19+ - Keep your own selection state, derive a ` selected ` modifier (and any ` range_* ` modifiers) from it, and pass them to ` modifiers ` so styling/ARIA stay aligned.
20+ - Ignore clicks on ` modifiers.disabled ` or ` modifiers.hidden ` to preserve accessibility.
21+ - For keyboard support, handle ` onDayKeyDown ` (Space/Enter) in the same way as ` onDayClick ` .
22+
2223## Examples
2324
2425### Week Selection
@@ -31,35 +32,53 @@ Note the use of the `startOfWeek` and `endOfWeek` functions from [date-fns](http
3132import React , { useState } from " react" ;
3233
3334import { endOfWeek , startOfWeek } from " date-fns" ;
34- import { DateRange , DayPicker , isDateInRange } from " react-day-picker" ;
35+ import {
36+ type DateRange ,
37+ DayPicker ,
38+ rangeIncludesDate ,
39+ } from " react-day-picker" ;
3540
3641/** Select the whole week when the day is clicked. */
3742export function CustomWeek() {
3843 const [selectedWeek, setSelectedWeek] = useState <DateRange | undefined >();
3944
4045 return (
4146 <DayPicker
47+ mode = " default"
4248 showWeekNumber
4349 showOutsideDays
4450 modifiers = { {
4551 selected: selectedWeek ,
4652 range_start: selectedWeek ?.from ,
4753 range_end: selectedWeek ?.to ,
4854 range_middle : (date : Date ) =>
49- selectedWeek
50- ? isDateInRange (date , selectedWeek , { excludeEnds: true })
51- : false ,
55+ selectedWeek ? rangeIncludesDate (selectedWeek , date , true ) : false ,
5256 }}
5357 onDayClick = { (day , modifiers ) => {
58+ if (modifiers .disabled || modifiers .hidden ) return ;
5459 if (modifiers .selected ) {
55- setSelectedWeek (undefined ); // Clear the selection if the day is already selected
60+ setSelectedWeek (undefined );
5661 return ;
5762 }
5863 setSelectedWeek ({
5964 from: startOfWeek (day ),
6065 to: endOfWeek (day ),
6166 });
6267 }}
68+ onDayKeyDown = { (day , modifiers , e ) => {
69+ if (e .key === " " || e .key === " Enter" ) {
70+ e .preventDefault ();
71+ if (modifiers .disabled || modifiers .hidden ) return ;
72+ if (modifiers .selected ) {
73+ setSelectedWeek (undefined );
74+ return ;
75+ }
76+ setSelectedWeek ({
77+ from: startOfWeek (day ),
78+ to: endOfWeek (day ),
79+ });
80+ }
81+ }}
6382 footer = {
6483 selectedWeek &&
6584 ` Week from ${selectedWeek ?.from ?.toLocaleDateString ()} to ${selectedWeek ?.to ?.toLocaleDateString ()} `
@@ -91,12 +110,17 @@ export function CustomSingle() {
91110 }
92111 return (
93112 <DayPicker
113+ mode = " default"
94114 modifiers = { modifiers }
95115 onDayClick = { (day , modifiers ) => {
96- if (modifiers .selected ) {
97- setSelectedDate (undefined );
98- } else {
99- setSelectedDate (day );
116+ if (modifiers .disabled || modifiers .hidden ) return ;
117+ setSelectedDate (modifiers .selected ? undefined : day );
118+ }}
119+ onDayKeyDown = { (day , modifiers , e ) => {
120+ if (e .key === " " || e .key === " Enter" ) {
121+ e .preventDefault ();
122+ if (modifiers .disabled || modifiers .hidden ) return ;
123+ setSelectedDate (modifiers .selected ? undefined : day );
100124 }
101125 }}
102126 footer = { selectedDate && ` You selected ${selectedDate .toDateString ()} ` }
@@ -117,18 +141,24 @@ Selecting multiple days is a bit more complex as it involves handling an array o
117141import { useState } from " react" ;
118142
119143import { isSameDay } from " date-fns" ;
120- import { DayMouseEventHandler , DayPicker } from " react-day-picker" ;
144+ import { DayEventHandler , DayPicker } from " react-day-picker" ;
121145
122146export function CustomMultiple() {
123147 const [value, setValue] = useState <Date []>([]);
124148
125- const handleDayClick: DayMouseEventHandler = (day , modifiers ) => {
149+ const handleDayClick: DayEventHandler <React .MouseEvent > = (
150+ day ,
151+ modifiers ,
152+ ) => {
153+ if (modifiers .disabled || modifiers .hidden ) return ;
126154 const newValue = [... value ];
127155 if (modifiers .selected ) {
128156 const index = value .findIndex ((d ) => isSameDay (day , d ));
129157 newValue .splice (index , 1 );
130158 } else {
131- newValue .push (day );
159+ if (! value .some ((d ) => isSameDay (d , day ))) {
160+ newValue .push (day );
161+ }
132162 }
133163 setValue (newValue );
134164 };
@@ -141,13 +171,22 @@ export function CustomMultiple() {
141171 footer = (
142172 <>
143173 You selected { value .length } days.{ " " }
144- <button onClick = { handleResetClick } >Reset</button >
174+ <button type = " button" onClick = { handleResetClick } >
175+ Reset
176+ </button >
145177 </>
146178 );
147179
148180 return (
149181 <DayPicker
182+ mode = " default"
150183 onDayClick = { handleDayClick }
184+ onDayKeyDown = { (day , modifiers , e ) => {
185+ if (e .key === " " || e .key === " Enter" ) {
186+ e .preventDefault ();
187+ handleDayClick (day , modifiers , e as any );
188+ }
189+ }}
151190 modifiers = { { selected: value }}
152191 footer = { footer }
153192 />
@@ -158,3 +197,145 @@ export function CustomMultiple() {
158197<BrowserWindow >
159198 <Examples.CustomMultiple />
160199</BrowserWindow >
200+
201+ ### Rolling N-day window
202+
203+ Create a fixed-length range (for example, 7 days) starting from the clicked day.
204+
205+ ``` tsx
206+ import { addDays } from " date-fns" ;
207+ import { useState } from " react" ;
208+
209+ import { type DateRange , DayPicker } from " react-day-picker" ;
210+
211+ export function CustomRollingWindow() {
212+ const [range, setRange] = useState <DateRange | undefined >();
213+ const windowLength = 7 ;
214+
215+ const applyRange = (start : Date ): DateRange => ({
216+ from: start ,
217+ to: addDays (start , windowLength - 1 ),
218+ });
219+
220+ return (
221+ <DayPicker
222+ mode = " default"
223+ modifiers = { {
224+ selected: range ,
225+ range_start: range ?.from ,
226+ range_end: range ?.to ,
227+ range_middle: range ,
228+ }}
229+ onDayClick = { (day , modifiers ) => {
230+ if (modifiers .disabled || modifiers .hidden ) return ;
231+ setRange (modifiers .selected ? undefined : applyRange (day ));
232+ }}
233+ onDayKeyDown = { (day , modifiers , e ) => {
234+ if (e .key === " " || e .key === " Enter" ) {
235+ e .preventDefault ();
236+ if (modifiers .disabled || modifiers .hidden ) return ;
237+ setRange (modifiers .selected ? undefined : applyRange (day ));
238+ }
239+ }}
240+ />
241+ );
242+ }
243+ ```
244+
245+ <BrowserWindow >
246+ <Examples.CustomRollingWindow />
247+ </BrowserWindow >
248+
249+ ### Selecting full months
250+
251+ Toggle an entire month at a time.
252+
253+ ``` tsx
254+ import { endOfMonth , startOfMonth } from " date-fns" ;
255+ import { useState } from " react" ;
256+
257+ import { type DateRange , DayPicker } from " react-day-picker" ;
258+
259+ export function CustomMonthSelection() {
260+ const [monthRange, setMonthRange] = useState <DateRange | undefined >();
261+
262+ const toMonthRange = (day : Date ): DateRange => ({
263+ from: startOfMonth (day ),
264+ to: endOfMonth (day ),
265+ });
266+
267+ const isInRange = (day : Date ) =>
268+ monthRange ?.from && monthRange ?.to
269+ ? day >= monthRange .from && day <= monthRange .to
270+ : false ;
271+
272+ return (
273+ <DayPicker
274+ mode = " default"
275+ showOutsideDays
276+ modifiers = { {
277+ selected: monthRange ,
278+ range_start: monthRange ?.from ,
279+ range_end: monthRange ?.to ,
280+ range_middle: monthRange ,
281+ }}
282+ onDayClick = { (day , modifiers ) => {
283+ if (modifiers .disabled || modifiers .hidden ) return ;
284+ setMonthRange (isInRange (day ) ? undefined : toMonthRange (day ));
285+ }}
286+ />
287+ );
288+ }
289+ ```
290+
291+ <BrowserWindow >
292+ <Examples.CustomMonthSelection />
293+ </BrowserWindow >
294+
295+ ## Style custom modifiers
296+
297+ Use ` modifiersClassNames ` or ` modifiersStyles ` to surface your custom state (for example, ` range_middle ` , ` rolling ` , ` blocked ` ).
298+
299+ ``` tsx
300+ <DayPicker
301+ mode = " default"
302+ modifiers = { { selected: value , rolling: rollingRange }}
303+ modifiersClassNames = { { rolling: " my-rolling" }}
304+ modifiersStyles = { { rolling: { background: " var(--rdp-accent-color)" } }}
305+ />
306+ ```
307+
308+ ## Week numbers and starts
309+
310+ For week-based selections, set ` weekStartsOn ` (locale-aware) and consider handling ` onWeekNumberClick ` to select a week when its number is clicked. Combine it with ` showWeekNumber ` .
311+
312+ ``` tsx
313+ <DayPicker
314+ mode = " default"
315+ showWeekNumber
316+ weekStartsOn = { 1 }
317+ onWeekNumberClick = { (weekNumber , week , e ) => {
318+ e .preventDefault ();
319+ setSelectedWeek ({ from: week .from , to: week .to });
320+ }}
321+ modifiers = { { selected: selectedWeek }}
322+ />
323+ ```
324+
325+ ## Persisting custom selections
326+
327+ Serialize your custom selection state before storing or sending it.
328+
329+ ``` ts
330+ // Single date
331+ const payload = selectedDate ? selectedDate .toISOString () : null ;
332+
333+ // Multiple dates
334+ const payload = value .map ((date ) => date .toISOString ());
335+
336+ // Range
337+ const payload =
338+ range ?.from && range ?.to
339+ ? { from: range .from .toISOString (), to: range .to .toISOString () }
340+ : null ;
341+ ```
0 commit comments