Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs-site/src/components/Examples/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import CustomInput from "../../examples/ts/customInput?raw";
import RenderCustomHeader from "../../examples/ts/renderCustomHeader?raw";
import RenderCustomHeaderTwoMonths from "../../examples/ts/renderCustomHeaderTwoMonths?raw";
import RenderCustomDayName from "../../examples/ts/renderCustomDayName?raw";
import MonthHeaderPosition from "../../examples/ts/monthHeaderPosition?raw";
import RenderCustomDay from "../../examples/ts/renderCustomDay?raw";
import RenderCustomMonth from "../../examples/ts/renderCustomMonth?raw";
import RenderCustomQuarter from "../../examples/ts/renderCustomQuarter?raw";
Expand Down Expand Up @@ -189,6 +190,10 @@ export const EXAMPLE_CONFIG: IExampleConfig[] = [
title: "Custom Day Names",
component: RenderCustomDayName,
},
{
title: "Month header position",
component: MonthHeaderPosition,
},
{
title: "Custom Day",
component: RenderCustomDay,
Expand Down
54 changes: 54 additions & 0 deletions docs-site/src/examples/ts/monthHeaderPosition.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
type Position = "top" | "middle" | "bottom";

const MonthHeaderPositionExample = () => {
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());
const [position, setPosition] = useState<Position>("middle");

return (
<>
<div
style={{
marginBottom: "20px",
display: "flex",
flexDirection: "column",
gap: "4px",
}}
>
<label>
<input
type="radio"
value="top"
checked={position === "top"}
onChange={(e) => setPosition(e.target.value as Position)}
/>
Top (default)
</label>
<label>
<input
type="radio"
value="middle"
checked={position === "middle"}
onChange={(e) => setPosition(e.target.value as Position)}
/>
Middle
</label>
<label>
<input
type="radio"
value="bottom"
checked={position === "bottom"}
onChange={(e) => setPosition(e.target.value as Position)}
/>
Bottom
</label>
</div>
<DatePicker
selected={selectedDate}
onChange={setSelectedDate}
monthHeaderPosition={position}
/>
</>
);
};

render(MonthHeaderPositionExample);
51 changes: 51 additions & 0 deletions docs/month_header_position.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# monthHeaderPosition

## Description

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").

## Type

```typescript
monthHeaderPosition?: "top" | "middle" | "bottom";
```

## Values

- `"top"` (or undefined) - Month header appears in the standard position at the top of the calendar (default)
- `"middle"` - Month header appears between day names and calendar days
- `"bottom"` - Month header appears at the bottom of the calendar

## Usage

```tsx
import React, { useState } from "react";
import DatePicker from "react-datepicker";

// Example 1: Header in the middle (between day names and days)
const MiddlePositionExample = () => {
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());

return <DatePicker selected={selectedDate} onChange={setSelectedDate} monthHeaderPosition="middle" />;
};

// Example 2: Header at the bottom
const BottomPositionExample = () => {
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());

return <DatePicker selected={selectedDate} onChange={setSelectedDate} monthHeaderPosition="bottom" />;
};

// Example 3: Default position (top)
const DefaultPositionExample = () => {
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());

return <DatePicker selected={selectedDate} onChange={setSelectedDate} monthHeaderPosition="top" />;
};
```

## Notes

- When `monthHeaderPosition` is set to `"middle"` or `"bottom"`, the month header (including navigation buttons and dropdowns) is removed from the default header section
- Works with multiple months (`monthsShown` prop) - each month's header will be positioned accordingly
- Navigation buttons are included and properly positioned in all three position options
72 changes: 53 additions & 19 deletions src/calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ type CalendarProps = React.PropsWithChildren<
renderCustomDayName?: (
props: ReactDatePickerCustomDayNameProps,
) => React.ReactNode;
monthHeaderPosition?: "top" | "middle" | "bottom";
onYearMouseEnter?: YearProps["onYearMouseEnter"];
onYearMouseLeave?: YearProps["onYearMouseLeave"];
monthAriaLabelPrefix?: MonthProps["ariaLabelPrefix"];
Expand Down Expand Up @@ -250,6 +251,7 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
previousMonthButtonLabel: "Previous Month",
nextMonthButtonLabel: "Next Month",
yearItemNumber: DEFAULT_YEAR_ITEM_NUMBER,
monthHeaderPosition: "top",
};
}

Expand Down Expand Up @@ -916,25 +918,44 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
</div>
);

renderDefaultHeader = ({ monthDate, i }: { monthDate: Date; i: number }) => (
<div
className={`react-datepicker__header ${
this.props.showTimeSelect
? "react-datepicker__header--has-time-select"
: ""
}`}
>
{this.renderCurrentMonth(monthDate)}
renderDefaultHeader = ({ monthDate, i }: { monthDate: Date; i: number }) => {
const headerContent = (
<div
className={`react-datepicker__header__dropdown react-datepicker__header__dropdown--${this.props.dropdownMode}`}
onFocus={this.handleDropdownFocus}
className={clsx("react-datepicker__header", {
"react-datepicker__header--has-time-select":
this.props.showTimeSelect,
"react-datepicker__header--middle":
this.props.monthHeaderPosition === "middle",
"react-datepicker__header--bottom":
this.props.monthHeaderPosition === "bottom",
})}
>
{this.renderMonthDropdown(i !== 0)}
{this.renderMonthYearDropdown(i !== 0)}
{this.renderYearDropdown(i !== 0)}
{this.renderCurrentMonth(monthDate)}
<div
className={`react-datepicker__header__dropdown react-datepicker__header__dropdown--${this.props.dropdownMode}`}
onFocus={this.handleDropdownFocus}
>
{this.renderMonthDropdown(i !== 0)}
{this.renderMonthYearDropdown(i !== 0)}
{this.renderYearDropdown(i !== 0)}
</div>
</div>
</div>
);
);

// Top position: render header directly in default location
if (this.props.monthHeaderPosition === "top") {
return headerContent;
}

// Middle/bottom positions: wrap with navigation buttons
return (
<div className="react-datepicker__header-wrapper">
{this.renderPreviousButton() || null}
{this.renderNextButton() || null}
{headerContent}
</div>
);
};

renderCustomHeader = (headerArgs: { monthDate: Date; i: number }) => {
const { monthDate, i } = headerArgs;
Expand Down Expand Up @@ -1078,7 +1099,8 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
}}
className="react-datepicker__month-container"
>
{this.renderHeader({ monthDate, i })}
{this.props.monthHeaderPosition === "top" &&
this.renderHeader({ monthDate, i })}
<Month
{...Calendar.defaultProps}
{...this.props}
Expand All @@ -1095,6 +1117,16 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
monthShowsDuplicateDaysEnd={monthShowsDuplicateDaysEnd}
monthShowsDuplicateDaysStart={monthShowsDuplicateDaysStart}
dayNamesHeader={this.renderDayNamesHeader(monthDate, i)}
monthHeader={
this.props.monthHeaderPosition === "middle"
? this.renderHeader({ monthDate, i })
: undefined
}
monthFooter={
this.props.monthHeaderPosition === "bottom"
? this.renderHeader({ monthDate, i })
: undefined
}
/>
</div>,
);
Expand Down Expand Up @@ -1239,8 +1271,10 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
inline={this.props.inline}
>
{this.renderAriaLiveRegion()}
{this.renderPreviousButton()}
{this.renderNextButton()}
{this.props.monthHeaderPosition === "top" &&
this.renderPreviousButton()}
{this.props.monthHeaderPosition === "top" &&
this.renderNextButton()}
{this.renderMonths()}
{this.renderYears()}
{this.renderTodayButton()}
Expand Down
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1822,6 +1822,7 @@ export class DatePicker extends Component<DatePickerProps, DatePickerState> {
popperComponent={calendar}
popperOnKeyDown={this.onPopperKeyDown}
showArrow={this.props.showPopperArrow}
monthHeaderPosition={this.props.monthHeaderPosition}
/>
);
}
Expand Down
8 changes: 8 additions & 0 deletions src/month.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ interface MonthProps extends Omit<
chooseDayAriaLabelPrefix?: WeekProps["chooseDayAriaLabelPrefix"];
disabledDayAriaLabelPrefix?: WeekProps["disabledDayAriaLabelPrefix"];
dayNamesHeader?: React.ReactNode;
monthHeader?: React.ReactNode;
monthFooter?: React.ReactNode;
}

/**
Expand Down Expand Up @@ -1174,6 +1176,9 @@ export default class Month extends Component<MonthProps> {
{this.props.dayNamesHeader && (
<div role="rowgroup">{this.props.dayNamesHeader}</div>
)}
{this.props.monthHeader && (
<div role="rowgroup">{this.props.monthHeader}</div>
)}
<div
className={this.getClassNames()}
onMouseLeave={
Expand All @@ -1187,6 +1192,9 @@ export default class Month extends Component<MonthProps> {
>
{this.renderWeeks()}
</div>
{this.props.monthFooter && (
<div role="rowgroup">{this.props.monthFooter}</div>
)}
</div>
);
}
Expand Down
6 changes: 6 additions & 0 deletions src/popper_component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ interface PopperComponentProps
popperOnKeyDown: React.KeyboardEventHandler<HTMLDivElement>;
showArrow?: boolean;
portalId?: PortalProps["portalId"];
monthHeaderPosition?: "top" | "middle" | "bottom";
}

// Exported for testing purposes
Expand All @@ -44,6 +45,7 @@ export const PopperComponent: React.FC<PopperComponentProps> = (props) => {
portalHost,
popperProps,
showArrow,
monthHeaderPosition,
} = props;

let popper: React.ReactElement | undefined = undefined;
Expand All @@ -52,6 +54,10 @@ export const PopperComponent: React.FC<PopperComponentProps> = (props) => {
const classes = clsx(
"react-datepicker-popper",
!showArrow && "react-datepicker-popper-offset",
monthHeaderPosition === "middle" &&
"react-datepicker-popper--header-middle",
monthHeaderPosition === "bottom" &&
"react-datepicker-popper--header-bottom",
className,
);
popper = (
Expand Down
46 changes: 45 additions & 1 deletion src/stylesheets/datepicker.scss
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,25 @@
color: #fff;
}
}

&--header-middle,
&--header-bottom {
&[data-placement^="bottom"] {
.react-datepicker__triangle {
fill: #fff;
color: #fff;
}
}
}

&--header-bottom {
&[data-placement^="top"] {
.react-datepicker__triangle {
fill: $datepicker__background-color;
color: $datepicker__background-color;
}
}
}
}

.react-datepicker__header {
Expand All @@ -90,9 +109,34 @@
}
}

&:not(&--has-time-select) {
&:not(&--has-time-select, &--middle, &--bottom) {
border-top-right-radius: $datepicker__border-radius;
}

// Header in middle position (between day names and days)
&--middle {
border-top: $datepicker__border;
border-radius: 0;
margin-top: 4px;
}

// Header in bottom position (at calendar bottom)
&--bottom {
border-bottom: none;
border-top: $datepicker__border;
border-radius: 0 0 $datepicker__border-radius $datepicker__border-radius;
}
}

// Wrapper for header in middle/bottom positions
.react-datepicker__header-wrapper {
position: relative;

.react-datepicker__navigation--next--with-time:not(
.react-datepicker__navigation--next--with-today-button
) {
right: 2px;
}
}

.react-datepicker__year-dropdown-container--select,
Expand Down
Loading
Loading