@Daniele thanks for the update and the explanation! While i couldnt see the video (it shows 0sec length) but everything works now!
One last note before the topic can be closed: I wrote a script with chatGPT that adds the necessary accessibility functions to flatpickr. probably not the cleanest and by it is by itself obv not optimal that flatpickr is not accessible but it works as a workaround. Maybe it helps anyone who stumbles upon this thread:
CSS
.fp-nav-button {
background: transparent;
border: none;
cursor: pointer;
}
/* Screen-reader–only text */
.fp-sr-desc {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
border: 0;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
Script
document.addEventListener('DOMContentLoaded', () => {
const DELAY = 400;
setTimeout(() => {
document.querySelectorAll('input.flatpickr.flatpickr-input').forEach(input => {
const fp = input._flatpickr;
if (!fp) return;
// Re-apply enhancements on every redraw
['onReady','onOpen','onMonthChange','onYearChange']
.forEach(hook => fp.config[hook].push(() => setTimeout(enhance, DELAY)));
// Initial enhancement
setTimeout(enhance, DELAY * 1.5);
function enhance() {
const cal = fp.calendarContainer;
if (!cal) return;
// 1) Dialog roles & input ARIA
cal.setAttribute('role', 'dialog');
cal.setAttribute('aria-label', 'Date picker');
input.setAttribute('aria-haspopup', 'grid');
const cid = cal.id || (cal.id = 'fp-' + Math.random().toString(36).slice(2));
input.setAttribute('aria-controls', cid);
input.setAttribute('aria-expanded', 'true');
// 2) Grid & cells
const dw = cal.querySelector('.flatpickr-days .dayContainer');
if (!dw) return;
const grid = dw.parentElement;
grid.setAttribute('role', 'grid');
const weekCount = cal.querySelectorAll('.flatpickr-weekday').length || 7;
const days = Array.from(dw.querySelectorAll('.flatpickr-day'));
days.forEach(d => {
d.setAttribute('role', 'gridcell');
d.setAttribute('aria-selected', d.classList.contains('selected') ? 'true' : 'false');
if (d.classList.contains('prevMonthDay') || d.classList.contains('nextMonthDay')) {
d.setAttribute('aria-disabled', 'true');
} else {
d.removeAttribute('aria-disabled');
}
d.tabIndex = -1;
});
const first = days.find(d => d.classList.contains('selected'))
|| days.find(d => d.classList.contains('today'))
|| days[0];
if (first) first.tabIndex = 0;
// 2a) Keyboard navigation + Shift-Tab
dw.onkeydown = e => {
if (e.key === 'Tab') {
e.stopPropagation();
const focusables = [
cal.querySelector('button.fp-nav-button[aria-label="Previous month"]'),
cal.querySelector('.flatpickr-monthDropdown-months'),
cal.querySelector('.numInput.cur-year'),
cal.querySelector('button.fp-nav-button[aria-label="Next month"]'),
dw.querySelector('[tabindex="0"]')
].filter(Boolean);
let idx = focusables.indexOf(document.activeElement);
if (idx === -1) idx = focusables.length - 1;
if (e.shiftKey) {
const prev = (idx - 1 + focusables.length) % focusables.length;
focusables[prev].focus();
e.preventDefault();
}
return;
}
// stop Splide/carousel listening
const navKeys = ['ArrowRight','ArrowLeft','ArrowDown','ArrowUp','Home','End','PageUp','PageDown','Enter',' '];
if (navKeys.includes(e.key)) e.stopPropagation();
const idx = days.indexOf(dw.querySelector('[tabindex="0"]'));
let next;
switch (e.key) {
case 'ArrowRight': next = days[idx+1]; break;
case 'ArrowLeft': next = days[idx-1]; break;
case 'ArrowDown': next = days[idx+weekCount]; break;
case 'ArrowUp': next = days[idx-weekCount]; break;
case 'Home': next = days[0]; break;
case 'End': next = days[days.length-1]; break;
case 'PageUp': fp.changeMonth(-1); return;
case 'PageDown': fp.changeMonth(1); return;
case 'Enter':
case ' ': dw.querySelector('[tabindex="0"]').click(); return;
default: return;
}
if (next) {
e.preventDefault();
const cur = dw.querySelector('[tabindex="0"]');
cur.tabIndex = -1;
next.tabIndex = 0;
next.focus();
}
};
// 3) Prev/Next buttons
[['.flatpickr-prev-month', -1, 'Previous month'],
['.flatpickr-next-month', 1, 'Next month']
].forEach(([sel, dir, label]) => {
const span = cal.querySelector(sel);
if (!span) return;
const btn = document.createElement('button');
btn.type = 'button';
btn.classList.add('fp-nav-button');
btn.setAttribute('aria-label', label);
btn.innerHTML = span.innerHTML;
btn.addEventListener('click', e => {
e.stopPropagation();
fp.changeMonth(dir);
});
btn.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
btn.click();
}
});
span.replaceWith(btn);
});
// 4) Month & year selectors
const monthSel = cal.querySelector('.flatpickr-monthDropdown-months');
if (monthSel) {
monthSel.tabIndex = 0;
monthSel.setAttribute('aria-label','Select month');
monthSel.addEventListener('keydown', e => e.stopPropagation());
}
const yearInp = cal.querySelector('.numInput.cur-year');
if (yearInp) {
yearInp.tabIndex = 0;
yearInp.setAttribute('aria-label','Select year');
yearInp.addEventListener('keydown', e => e.stopPropagation());
}
// 5) Screen-reader instructions
if (!cal.querySelector('.fp-sr-desc')) {
const desc = document.createElement('div');
desc.id = cal.id + '-desc';
desc.className = 'fp-sr-desc';
desc.textContent = 'Use arrow keys to navigate dates; PageUp/PageDown to change month; Home/End for first/last day; Enter or space to select.';
cal.appendChild(desc);
grid.setAttribute('aria-describedby', desc.id);
}
}
});
}, DELAY);
});