+ ${this.i18n.monthNamesShort.map((label, index) => {
+ const value = yearMonthToValue({
+ year: this.currentYear,
+ month: index + 1,
+ });
+ const disabled = isInvalid(value, this.minYear, this.maxYear);
+ const selected = this.value === value;
+
+ return html`
+
+ ${label}
+
+ `;
+ })}
+
+ `;
+ }
+
+ /** @protected */
+ updated(props) {
+ super.updated(props);
+
+ if (props.has('currentYear')) {
+ this._yearLabel.textContent = this.currentYear;
+ }
+
+ if (props.has('currentYear') || props.has('minYear') || props.has('maxYear')) {
+ this._prevButton.disabled = isYearDisabled(this.currentYear - 1, this.minYear, this.maxYear);
+ this._nextButton.disabled = isYearDisabled(this.currentYear + 1, this.minYear, this.maxYear);
+ }
+
+ if (props.has('i18n')) {
+ this._prevButton.setAttribute('aria-label', this.i18n.prevYear);
+ this._nextButton.setAttribute('aria-label', this.i18n.nextYear);
+ }
+ }
+
+ /** @private */
+ __computeMonthPart(disabled, selected) {
+ const result = ['month'];
+
+ if (disabled) {
+ result.push('disabled-month');
+ }
+
+ if (selected) {
+ result.push('selected-month');
+ }
+
+ return result.join(' ');
+ }
+
+ /**
+ * @protected
+ * @override
+ */
+ _onKeyDown(event) {
+ super._onKeyDown(event);
+
+ if (event.key === 'Tab') {
+ // Prevent from bubbling to the host
+ event.stopPropagation();
+
+ const target = event.composedPath()[0];
+
+ // Tab / Shift + Tab on previous / next year button
+ if ((target === this._prevButton && !event.shiftKey) || (target === this._nextButton && event.shiftKey)) {
+ event.preventDefault();
+ // TODO: current behavior is implemented based on the add-on
+ // where months are only focusable within the current year.
+ // This can be an a11y issue, consider changing the logic.
+ if (this.focusedMonth) {
+ this.focusedMonth.focus();
+ } else if (target === this._prevButton && !event.shiftKey) {
+ this._nextButton.focus();
+ } else if (target === this._nextButton && event.shiftKey) {
+ this._prevButton.focus();
+ }
+ }
+
+ // Tab on next year button, focus the input field
+ if (target === this._nextButton && !event.shiftKey) {
+ event.preventDefault();
+ this.owner.focus();
+ }
+
+ // Tab / Shift + Tab on the month cell
+ if (target.getAttribute('role') === 'gridcell') {
+ event.preventDefault();
+ event.stopPropagation();
+ const button = event.shiftKey ? this._prevButton : this._nextButton;
+ button.focus();
+ }
+ }
+ }
+
+ /** @private */
+ _onMonthClick(event) {
+ const { value } = event.target.dataset;
+ event.preventDefault();
+ this.dispatchEvent(new CustomEvent('month-click', { detail: { value } }));
+ }
+
+ /** @private */
+ __onMonthKeyDown(event) {
+ if (event.key === 'Tab') {
+ // Tab is handled separately
+ return;
+ }
+
+ const months = [...this.shadowRoot.querySelectorAll('[part~="month"]:not([disabled])')];
+
+ const focusedButton = this.shadowRoot.activeElement;
+ const currentIndex = months.indexOf(focusedButton);
+
+ if (currentIndex === -1) {
+ return;
+ }
+
+ let newIndex = currentIndex;
+
+ // TODO: add RTL support for Arrow Left / Arrow Right
+ switch (event.key) {
+ case 'ArrowLeft':
+ newIndex = currentIndex > 0 ? currentIndex - 1 : currentIndex;
+ break;
+ case 'ArrowRight':
+ newIndex = currentIndex < months.length - 1 ? currentIndex + 1 : currentIndex;
+ break;
+ case 'ArrowUp':
+ newIndex = currentIndex - 4 >= 0 ? currentIndex - 4 : currentIndex;
+ break;
+ case 'ArrowDown':
+ newIndex = currentIndex + 4 < months.length ? currentIndex + 4 : currentIndex;
+ break;
+ case 'Enter':
+ this._onMonthClick(event);
+ break;
+ case ' ':
+ this._onMonthClick(event);
+ break;
+ default:
+ break;
+ }
+
+ if (newIndex !== currentIndex) {
+ event.preventDefault();
+ months[newIndex].focus();
+ }
+ }
+
+ /** @private */
+ _onPrevYearClick() {
+ this.currentYear -= 1;
+ }
+
+ /** @private */
+ _onNextYearClick() {
+ this.currentYear += 1;
+ }
+}
+
+defineCustomElement(MonthPickerOverlayContent);
+
+export { MonthPickerOverlayContent };
diff --git a/packages/month-picker/src/vaadin-month-picker-overlay.js b/packages/month-picker/src/vaadin-month-picker-overlay.js
new file mode 100644
index 0000000000..2bae9ec8b1
--- /dev/null
+++ b/packages/month-picker/src/vaadin-month-picker-overlay.js
@@ -0,0 +1,62 @@
+/**
+ * @license
+ * Copyright (c) 2025 - 2025 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+import { html, LitElement } from 'lit';
+import { defineCustomElement } from '@vaadin/component-base/src/define.js';
+import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js';
+import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
+import { overlayStyles } from '@vaadin/overlay/src/styles/vaadin-overlay-base-styles.js';
+import { OverlayMixin } from '@vaadin/overlay/src/vaadin-overlay-mixin.js';
+import { PositionMixin } from '@vaadin/overlay/src/vaadin-overlay-position-mixin.js';
+import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js';
+import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
+import { monthPickerOverlayStyles } from './styles/vaadin-month-picker-overlay-base-styles.js';
+
+/**
+ * An element used internally by `