Skip to content

Commit 0e13929

Browse files
Merge pull request #6092 from gnato/feature/month-header-placement
feat: Add monthHeaderPosition prop for flexible calendar header placement
2 parents 11aeae6 + 09e92cc commit 0e13929

File tree

11 files changed

+610
-20
lines changed

11 files changed

+610
-20
lines changed

docs-site/src/components/Examples/config.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import CustomInput from "../../examples/ts/customInput?raw";
1818
import RenderCustomHeader from "../../examples/ts/renderCustomHeader?raw";
1919
import RenderCustomHeaderTwoMonths from "../../examples/ts/renderCustomHeaderTwoMonths?raw";
2020
import RenderCustomDayName from "../../examples/ts/renderCustomDayName?raw";
21+
import MonthHeaderPosition from "../../examples/ts/monthHeaderPosition?raw";
2122
import RenderCustomDay from "../../examples/ts/renderCustomDay?raw";
2223
import RenderCustomMonth from "../../examples/ts/renderCustomMonth?raw";
2324
import RenderCustomQuarter from "../../examples/ts/renderCustomQuarter?raw";
@@ -189,6 +190,10 @@ export const EXAMPLE_CONFIG: IExampleConfig[] = [
189190
title: "Custom Day Names",
190191
component: RenderCustomDayName,
191192
},
193+
{
194+
title: "Month header position",
195+
component: MonthHeaderPosition,
196+
},
192197
{
193198
title: "Custom Day",
194199
component: RenderCustomDay,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
type Position = "top" | "middle" | "bottom";
2+
3+
const MonthHeaderPositionExample = () => {
4+
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());
5+
const [position, setPosition] = useState<Position>("middle");
6+
7+
return (
8+
<>
9+
<div
10+
style={{
11+
marginBottom: "20px",
12+
display: "flex",
13+
flexDirection: "column",
14+
gap: "4px",
15+
}}
16+
>
17+
<label>
18+
<input
19+
type="radio"
20+
value="top"
21+
checked={position === "top"}
22+
onChange={(e) => setPosition(e.target.value as Position)}
23+
/>
24+
Top (default)
25+
</label>
26+
<label>
27+
<input
28+
type="radio"
29+
value="middle"
30+
checked={position === "middle"}
31+
onChange={(e) => setPosition(e.target.value as Position)}
32+
/>
33+
Middle
34+
</label>
35+
<label>
36+
<input
37+
type="radio"
38+
value="bottom"
39+
checked={position === "bottom"}
40+
onChange={(e) => setPosition(e.target.value as Position)}
41+
/>
42+
Bottom
43+
</label>
44+
</div>
45+
<DatePicker
46+
selected={selectedDate}
47+
onChange={setSelectedDate}
48+
monthHeaderPosition={position}
49+
/>
50+
</>
51+
);
52+
};
53+
54+
render(MonthHeaderPositionExample);

docs/month_header_position.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# monthHeaderPosition
2+
3+
## Description
4+
5+
The `monthHeaderPosition` prop allows you to control where the month header (e.g., "December 2025") is displayed in the calendar. By default, it appears in the standard header section above the day names. You can reposition the header to appear between the day names and calendar days ("middle") or at the bottom of the calendar ("bottom").
6+
7+
## Type
8+
9+
```typescript
10+
monthHeaderPosition?: "top" | "middle" | "bottom";
11+
```
12+
13+
## Values
14+
15+
- `"top"` (or undefined) - Month header appears in the standard position at the top of the calendar (default)
16+
- `"middle"` - Month header appears between day names and calendar days
17+
- `"bottom"` - Month header appears at the bottom of the calendar
18+
19+
## Usage
20+
21+
```tsx
22+
import React, { useState } from "react";
23+
import DatePicker from "react-datepicker";
24+
25+
// Example 1: Header in the middle (between day names and days)
26+
const MiddlePositionExample = () => {
27+
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());
28+
29+
return <DatePicker selected={selectedDate} onChange={setSelectedDate} monthHeaderPosition="middle" />;
30+
};
31+
32+
// Example 2: Header at the bottom
33+
const BottomPositionExample = () => {
34+
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());
35+
36+
return <DatePicker selected={selectedDate} onChange={setSelectedDate} monthHeaderPosition="bottom" />;
37+
};
38+
39+
// Example 3: Default position (top)
40+
const DefaultPositionExample = () => {
41+
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());
42+
43+
return <DatePicker selected={selectedDate} onChange={setSelectedDate} monthHeaderPosition="top" />;
44+
};
45+
```
46+
47+
## Notes
48+
49+
- When `monthHeaderPosition` is set to `"middle"` or `"bottom"`, the month header (including navigation buttons and dropdowns) is removed from the default header section
50+
- Works with multiple months (`monthsShown` prop) - each month's header will be positioned accordingly
51+
- Navigation buttons are included and properly positioned in all three position options

src/calendar.tsx

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ type CalendarProps = React.PropsWithChildren<
206206
renderCustomDayName?: (
207207
props: ReactDatePickerCustomDayNameProps,
208208
) => React.ReactNode;
209+
monthHeaderPosition?: "top" | "middle" | "bottom";
209210
onYearMouseEnter?: YearProps["onYearMouseEnter"];
210211
onYearMouseLeave?: YearProps["onYearMouseLeave"];
211212
monthAriaLabelPrefix?: MonthProps["ariaLabelPrefix"];
@@ -250,6 +251,7 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
250251
previousMonthButtonLabel: "Previous Month",
251252
nextMonthButtonLabel: "Next Month",
252253
yearItemNumber: DEFAULT_YEAR_ITEM_NUMBER,
254+
monthHeaderPosition: "top",
253255
};
254256
}
255257

@@ -916,25 +918,44 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
916918
</div>
917919
);
918920

919-
renderDefaultHeader = ({ monthDate, i }: { monthDate: Date; i: number }) => (
920-
<div
921-
className={`react-datepicker__header ${
922-
this.props.showTimeSelect
923-
? "react-datepicker__header--has-time-select"
924-
: ""
925-
}`}
926-
>
927-
{this.renderCurrentMonth(monthDate)}
921+
renderDefaultHeader = ({ monthDate, i }: { monthDate: Date; i: number }) => {
922+
const headerContent = (
928923
<div
929-
className={`react-datepicker__header__dropdown react-datepicker__header__dropdown--${this.props.dropdownMode}`}
930-
onFocus={this.handleDropdownFocus}
924+
className={clsx("react-datepicker__header", {
925+
"react-datepicker__header--has-time-select":
926+
this.props.showTimeSelect,
927+
"react-datepicker__header--middle":
928+
this.props.monthHeaderPosition === "middle",
929+
"react-datepicker__header--bottom":
930+
this.props.monthHeaderPosition === "bottom",
931+
})}
931932
>
932-
{this.renderMonthDropdown(i !== 0)}
933-
{this.renderMonthYearDropdown(i !== 0)}
934-
{this.renderYearDropdown(i !== 0)}
933+
{this.renderCurrentMonth(monthDate)}
934+
<div
935+
className={`react-datepicker__header__dropdown react-datepicker__header__dropdown--${this.props.dropdownMode}`}
936+
onFocus={this.handleDropdownFocus}
937+
>
938+
{this.renderMonthDropdown(i !== 0)}
939+
{this.renderMonthYearDropdown(i !== 0)}
940+
{this.renderYearDropdown(i !== 0)}
941+
</div>
935942
</div>
936-
</div>
937-
);
943+
);
944+
945+
// Top position: render header directly in default location
946+
if (this.props.monthHeaderPosition === "top") {
947+
return headerContent;
948+
}
949+
950+
// Middle/bottom positions: wrap with navigation buttons
951+
return (
952+
<div className="react-datepicker__header-wrapper">
953+
{this.renderPreviousButton() || null}
954+
{this.renderNextButton() || null}
955+
{headerContent}
956+
</div>
957+
);
958+
};
938959

939960
renderCustomHeader = (headerArgs: { monthDate: Date; i: number }) => {
940961
const { monthDate, i } = headerArgs;
@@ -1078,7 +1099,8 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
10781099
}}
10791100
className="react-datepicker__month-container"
10801101
>
1081-
{this.renderHeader({ monthDate, i })}
1102+
{this.props.monthHeaderPosition === "top" &&
1103+
this.renderHeader({ monthDate, i })}
10821104
<Month
10831105
{...Calendar.defaultProps}
10841106
{...this.props}
@@ -1095,6 +1117,16 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
10951117
monthShowsDuplicateDaysEnd={monthShowsDuplicateDaysEnd}
10961118
monthShowsDuplicateDaysStart={monthShowsDuplicateDaysStart}
10971119
dayNamesHeader={this.renderDayNamesHeader(monthDate, i)}
1120+
monthHeader={
1121+
this.props.monthHeaderPosition === "middle"
1122+
? this.renderHeader({ monthDate, i })
1123+
: undefined
1124+
}
1125+
monthFooter={
1126+
this.props.monthHeaderPosition === "bottom"
1127+
? this.renderHeader({ monthDate, i })
1128+
: undefined
1129+
}
10981130
/>
10991131
</div>,
11001132
);
@@ -1239,8 +1271,10 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
12391271
inline={this.props.inline}
12401272
>
12411273
{this.renderAriaLiveRegion()}
1242-
{this.renderPreviousButton()}
1243-
{this.renderNextButton()}
1274+
{this.props.monthHeaderPosition === "top" &&
1275+
this.renderPreviousButton()}
1276+
{this.props.monthHeaderPosition === "top" &&
1277+
this.renderNextButton()}
12441278
{this.renderMonths()}
12451279
{this.renderYears()}
12461280
{this.renderTodayButton()}

src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1822,6 +1822,7 @@ export class DatePicker extends Component<DatePickerProps, DatePickerState> {
18221822
popperComponent={calendar}
18231823
popperOnKeyDown={this.onPopperKeyDown}
18241824
showArrow={this.props.showPopperArrow}
1825+
monthHeaderPosition={this.props.monthHeaderPosition}
18251826
/>
18261827
);
18271828
}

src/month.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ interface MonthProps extends Omit<
141141
chooseDayAriaLabelPrefix?: WeekProps["chooseDayAriaLabelPrefix"];
142142
disabledDayAriaLabelPrefix?: WeekProps["disabledDayAriaLabelPrefix"];
143143
dayNamesHeader?: React.ReactNode;
144+
monthHeader?: React.ReactNode;
145+
monthFooter?: React.ReactNode;
144146
}
145147

146148
/**
@@ -1174,6 +1176,9 @@ export default class Month extends Component<MonthProps> {
11741176
{this.props.dayNamesHeader && (
11751177
<div role="rowgroup">{this.props.dayNamesHeader}</div>
11761178
)}
1179+
{this.props.monthHeader && (
1180+
<div role="rowgroup">{this.props.monthHeader}</div>
1181+
)}
11771182
<div
11781183
className={this.getClassNames()}
11791184
onMouseLeave={
@@ -1187,6 +1192,9 @@ export default class Month extends Component<MonthProps> {
11871192
>
11881193
{this.renderWeeks()}
11891194
</div>
1195+
{this.props.monthFooter && (
1196+
<div role="rowgroup">{this.props.monthFooter}</div>
1197+
)}
11901198
</div>
11911199
);
11921200
}

src/popper_component.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ interface PopperComponentProps
2828
popperOnKeyDown: React.KeyboardEventHandler<HTMLDivElement>;
2929
showArrow?: boolean;
3030
portalId?: PortalProps["portalId"];
31+
monthHeaderPosition?: "top" | "middle" | "bottom";
3132
}
3233

3334
// Exported for testing purposes
@@ -44,6 +45,7 @@ export const PopperComponent: React.FC<PopperComponentProps> = (props) => {
4445
portalHost,
4546
popperProps,
4647
showArrow,
48+
monthHeaderPosition,
4749
} = props;
4850

4951
let popper: React.ReactElement | undefined = undefined;
@@ -52,6 +54,10 @@ export const PopperComponent: React.FC<PopperComponentProps> = (props) => {
5254
const classes = clsx(
5355
"react-datepicker-popper",
5456
!showArrow && "react-datepicker-popper-offset",
57+
monthHeaderPosition === "middle" &&
58+
"react-datepicker-popper--header-middle",
59+
monthHeaderPosition === "bottom" &&
60+
"react-datepicker-popper--header-bottom",
5561
className,
5662
);
5763
popper = (

src/stylesheets/datepicker.scss

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,25 @@
7070
color: #fff;
7171
}
7272
}
73+
74+
&--header-middle,
75+
&--header-bottom {
76+
&[data-placement^="bottom"] {
77+
.react-datepicker__triangle {
78+
fill: #fff;
79+
color: #fff;
80+
}
81+
}
82+
}
83+
84+
&--header-bottom {
85+
&[data-placement^="top"] {
86+
.react-datepicker__triangle {
87+
fill: $datepicker__background-color;
88+
color: $datepicker__background-color;
89+
}
90+
}
91+
}
7392
}
7493

7594
.react-datepicker__header {
@@ -90,9 +109,34 @@
90109
}
91110
}
92111

93-
&:not(&--has-time-select) {
112+
&:not(&--has-time-select, &--middle, &--bottom) {
94113
border-top-right-radius: $datepicker__border-radius;
95114
}
115+
116+
// Header in middle position (between day names and days)
117+
&--middle {
118+
border-top: $datepicker__border;
119+
border-radius: 0;
120+
margin-top: 4px;
121+
}
122+
123+
// Header in bottom position (at calendar bottom)
124+
&--bottom {
125+
border-bottom: none;
126+
border-top: $datepicker__border;
127+
border-radius: 0 0 $datepicker__border-radius $datepicker__border-radius;
128+
}
129+
}
130+
131+
// Wrapper for header in middle/bottom positions
132+
.react-datepicker__header-wrapper {
133+
position: relative;
134+
135+
.react-datepicker__navigation--next--with-time:not(
136+
.react-datepicker__navigation--next--with-today-button
137+
) {
138+
right: 2px;
139+
}
96140
}
97141

98142
.react-datepicker__year-dropdown-container--select,

0 commit comments

Comments
 (0)