Skip to content

Commit 2a85e2b

Browse files
authored
docs: enhance custom components and custom selections guides (#2876)
* docs: enhance custom components guide * docs: flesh out date time picker guide * docs: expand custom selections guide with runnable examples * Lint files Signed-off-by: gpbl <[email protected]> * docs: fix custom selection examples without default mode --------- Signed-off-by: gpbl <[email protected]>
1 parent 4988ad5 commit 2a85e2b

File tree

7 files changed

+454
-31
lines changed

7 files changed

+454
-31
lines changed

examples/CustomMonthSelection.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { endOfMonth, startOfMonth } from "date-fns";
2+
import React, { useState } from "react";
3+
4+
import { type DateRange, DayPicker } from "react-day-picker";
5+
6+
/** Toggle selection of an entire month. */
7+
export function CustomMonthSelection() {
8+
const [monthRange, setMonthRange] = useState<DateRange | undefined>();
9+
10+
const toMonthRange = (day: Date): DateRange => ({
11+
from: startOfMonth(day),
12+
to: endOfMonth(day),
13+
});
14+
15+
const isInRange = (day: Date) =>
16+
monthRange?.from && monthRange?.to
17+
? day >= monthRange.from && day <= monthRange.to
18+
: false;
19+
20+
return (
21+
<DayPicker
22+
showOutsideDays
23+
modifiers={{
24+
selected: monthRange,
25+
range_start: monthRange?.from,
26+
range_end: monthRange?.to,
27+
range_middle: monthRange,
28+
}}
29+
onDayClick={(day, modifiers) => {
30+
if (modifiers.disabled || modifiers.hidden) return;
31+
setMonthRange(isInRange(day) ? undefined : toMonthRange(day));
32+
}}
33+
/>
34+
);
35+
}

examples/CustomRollingWindow.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { addDays } from "date-fns";
2+
import React, { useState } from "react";
3+
4+
import { type DateRange, DayPicker } from "react-day-picker";
5+
6+
/** Select a fixed-length range starting from the clicked day. */
7+
export function CustomRollingWindow() {
8+
const [range, setRange] = useState<DateRange | undefined>();
9+
const windowLength = 7;
10+
11+
const applyRange = (start: Date): DateRange => ({
12+
from: start,
13+
to: addDays(start, windowLength - 1),
14+
});
15+
16+
return (
17+
<DayPicker
18+
modifiers={{
19+
selected: range,
20+
range_start: range?.from,
21+
range_end: range?.to,
22+
range_middle: range,
23+
}}
24+
onDayClick={(day, modifiers) => {
25+
if (modifiers.disabled || modifiers.hidden) return;
26+
setRange(modifiers.selected ? undefined : applyRange(day));
27+
}}
28+
onDayKeyDown={(day, modifiers, e) => {
29+
if (e.key === " " || e.key === "Enter") {
30+
e.preventDefault();
31+
if (modifiers.disabled || modifiers.hidden) return;
32+
setRange(modifiers.selected ? undefined : applyRange(day));
33+
}
34+
}}
35+
/>
36+
);
37+
}

examples/InputTime.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
1-
import { setHours, setMinutes } from "date-fns";
2-
import React, { type ChangeEventHandler, useState } from "react";
1+
import { format, setHours, setMinutes } from "date-fns";
2+
import React, { type ChangeEventHandler, useEffect, useState } from "react";
33
import { DayPicker } from "react-day-picker";
44

55
export function InputTime() {
66
const [selected, setSelected] = useState<Date>();
77
const [timeValue, setTimeValue] = useState<string>("00:00");
88

9+
// Keep the time input in sync when the selected date changes elsewhere.
10+
useEffect(() => {
11+
if (selected) {
12+
setTimeValue(format(selected, "HH:mm"));
13+
}
14+
}, [selected]);
15+
916
const handleTimeChange: ChangeEventHandler<HTMLInputElement> = (e) => {
1017
const time = e.target.value;
1118
if (!selected) {
19+
// Defer composing a full Date until a day is picked.
1220
setTimeValue(time);
1321
return;
1422
}
1523
const [hours, minutes] = time.split(":").map((str) => parseInt(str, 10));
24+
// Compose a new Date using the current day plus the chosen time.
1625
const newSelectedDate = setHours(setMinutes(selected, minutes), hours);
1726
setSelected(newSelectedDate);
1827
setTimeValue(time);
@@ -26,6 +35,7 @@ export function InputTime() {
2635
const [hours, minutes] = timeValue
2736
.split(":")
2837
.map((str) => parseInt(str, 10));
38+
// Apply the time value to the picked day.
2939
const newDate = new Date(
3040
date.getFullYear(),
3141
date.getMonth(),

examples/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ export * from "./CssVariables";
1515
export * from "./CustomCaption";
1616
export * from "./CustomDayButton";
1717
export * from "./CustomDropdown";
18+
export * from "./CustomMonthSelection";
1819
export * from "./CustomMultiple";
20+
export * from "./CustomRollingWindow";
1921
export * from "./CustomSingle";
2022
export * from "./CustomWeek";
2123
export * from "./DefaultMonth";

website/docs/guides/custom-components.mdx

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ You may need to use custom components in advanced applications to:
1818
- Implement buttons or dropdowns using your own design system components.
1919
- Wrap an element with another element, like adding a tooltip to a day cell.
2020

21+
## Keep accessibility and behavior intact
22+
23+
- Always forward the props you receive (including `aria-*`, `tabIndex`, `ref`, and event handlers) to preserve keyboard navigation and screen reader support.
24+
- Reuse `classNames` and `labels` from `useDayPicker` when rendering built-in elements so modifiers and ARIA text remain correct.
25+
- Prefer composing around the default components instead of rebuilding behavior like focus management in `DayButton`.
26+
2127
When customizing components, familiarize yourself with the [API Reference](../api#components) and the [DayPicker Anatomy](../docs/anatomy.mdx). Ensure you maintain [accessibility](../guides/accessibility.mdx).
2228

2329
:::note Custom Components vs Formatters
@@ -42,6 +48,18 @@ Custom components allow you to extend DayPicker beyond just using [formatters](.
4248
/>
4349
```
4450

51+
### Component props cheat sheet
52+
53+
| Component | Props type | Notes |
54+
| ----------- | ------------------------------------------------- | ----------------------------------------------------- |
55+
| `Day` | [`DayProps`](../api/functions/Day.md) | Receives `day` (with `date`) and `modifiers`. |
56+
| `DayButton` | [`DayButtonProps`](../api/functions/DayButton.md) | Handles focus; keep forwarding `ref`/`aria-*`. |
57+
| `Nav` | [`NavProps`](../api/functions/Nav.md) | Use provided `onPreviousClick`/`onNextClick`. |
58+
| `Dropdown` | [`DropdownProps`](../api/functions/Dropdown.md) | Forward `aria-label` and call `onChange` with target. |
59+
| `Root` | [`RootProps`](../api/functions/Root.md) | Forward `rootRef` when `animate` is enabled. |
60+
61+
The `components` prop accepts a **partial** map—pass only the entries you want to override. The legacy `Button` entry is deprecated; prefer `NextMonthButton` and `PreviousMonthButton`.
62+
4563
## List of Custom Components
4664

4765
| Name | Description |
@@ -71,6 +89,7 @@ Custom components allow you to extend DayPicker beyond just using [formatters](.
7189
| [`Weekdays`](../api/functions/Weekdays.md) | The row containing the week days. |
7290
| [`Weeks`](../api/functions/Weeks.md) | The weeks section in the month grid. |
7391
| [`YearsDropdown`](../api/functions/YearsDropdown.md) | The dropdown with the years. |
92+
| `Button` | Deprecated: use `NextMonthButton`/`PreviousMonthButton`. |
7493

7594
## DayPicker Hook
7695

@@ -160,14 +179,39 @@ export function MyDatePicker() {
160179
<Examples.CustomDayButton />
161180
</BrowserWindow>
162181

182+
### Compose with the defaults
183+
184+
When you want to add UI but keep the built-in behavior (focus, labels, modifier classes), wrap the default component from context.
185+
186+
```tsx title="./WrappedDayButton.tsx"
187+
import { DayButtonProps, DayPicker, UI, useDayPicker } from "react-day-picker";
188+
189+
function WrappedDayButton(props: DayButtonProps) {
190+
const { components, classNames } = useDayPicker();
191+
return (
192+
<components.DayButton
193+
{...props}
194+
className={`${classNames[UI.DayButton]} my-custom-class`}
195+
>
196+
<span>{props.day.date.getDate()}</span>
197+
<small aria-hidden>★</small>
198+
</components.DayButton>
199+
);
200+
}
201+
202+
export function WrappedDayExample() {
203+
return <DayPicker components={{ DayButton: WrappedDayButton }} />;
204+
}
205+
```
206+
163207
### Custom Select Dropdown
164208

165209
You can use a custom [Dropdown](../api/functions/Dropdown.md) to select years and months. The example below uses a simplified version of `shadcn/ui`'s [Select](https://ui.shadcn.com/docs/components/select).
166210

167211
```tsx title="./CustomDropdown.tsx"
168212
import React, { useState } from "react";
169213

170-
import { DayButtonProps, DayPicker } from "react-day-picker";
214+
import { DayPicker, type DropdownProps } from "react-day-picker";
171215

172216
import {
173217
Select,
@@ -179,7 +223,7 @@ import {
179223
} from "@/components/ui/select";
180224

181225
export function CustomSelectDropdown(props: DropdownProps) {
182-
const { options, value, onChange } = props;
226+
const { options, value, onChange, "aria-label": ariaLabel } = props;
183227

184228
const handleValueChange = (newValue: string) => {
185229
if (onChange) {
@@ -195,7 +239,7 @@ export function CustomSelectDropdown(props: DropdownProps) {
195239

196240
return (
197241
<Select value={value?.toString()} onValueChange={handleValueChange}>
198-
<SelectTrigger>
242+
<SelectTrigger aria-label={ariaLabel}>
199243
<SelectValue />
200244
</SelectTrigger>
201245
<SelectContent>
@@ -237,4 +281,33 @@ export function CustomDropdown() {
237281
<Examples.CustomDropdown />
238282
</BrowserWindow>
239283

284+
If your design system portals dropdown content, provide the container (as shown in `examples/CustomDropdown/CustomDropdown.tsx`) so the list renders inside the calendar's shadow root in the docs.
285+
286+
### Structural customization
287+
288+
Swap layout components to add design-system wrappers or decoration while keeping behavior.
289+
290+
```tsx title="./CustomRoot.tsx"
291+
import { DayPicker, type RootProps } from "react-day-picker";
292+
293+
function CardRoot(props: RootProps) {
294+
const { rootRef, ...rest } = props;
295+
return (
296+
<div ref={rootRef} className="card shadow-md" {...rest}>
297+
{rest.children}
298+
</div>
299+
);
300+
}
301+
302+
export function CustomRootExample() {
303+
return <DayPicker components={{ Root: CardRoot }} />;
304+
}
305+
```
306+
307+
### Choosing Day vs. DayButton vs. formatters
308+
309+
- Use `DayButton` to change interactivity or add content inside the button while keeping the cell layout.
310+
- Use `Day` to change the table cell structure (wrappers, tooltips) when you need control over the `<td>`.
311+
- Use [formatters](./translation.mdx#custom-formatters) for simple text changes (e.g., labels) without altering structure.
312+
240313
What are you using custom components for? [Let us know](https://github.com/gpbl/react-day-picker/discussions).

0 commit comments

Comments
 (0)