Booking a flight, filing a claim, scheduling an appointment. Date entry sits in front of tasks that matter, and it is one of the least reliably accessible widgets on the web. The reason is structural rather than careless. A calendar is a two-dimensional grid, and grid keyboard navigation is a pattern most implementations never attempt.
Always Keep the Text Input
This is the most useful sentence on the page. Somebody who can type a date should be able to type it, and forcing everybody through a calendar to reach a date eleven months out is slow for a mouse user and punishing for a keyboard, screen reader or speech input user.
It also matters that there is nothing official to copy. The Authoring Practices Guide covers thirty component patterns and the date picker is not among them. What exists is a date picker dialog filed as an example under the Dialog pattern, and an example is not a pattern. Treat it as one worked answer rather than as the shape a conforming date picker has to take.
Accept several formats and normalize them yourself rather than rejecting input a person clearly meant. State the format you expect, and tie it to the field with aria-describedby rather than leaving it as a hopeful sentence nearby, which is the difference between a hint that reaches somebody and one that does not. W3C's own worked example puts the words date format, mm/dd/yyyy, in a span that the input points at. Never let a placeholder carry that on its own, because it disappears the moment somebody types. Our fix guide covers attaching help text with aria-describedby.
The native input, honestly
input type="date" gives you a picker the browser draws, and it normalizes the value to a single format however it is displayed, so you stop parsing dates by hand. Its min, max and step attributes disable out-of-range days for you and refuse an out-of-bounds submission, which is a chunk of work you were about to write. What we will not tell you is that it is accessible everywhere, because the documentation carries no accessibility section for it and we have not tested it across screen readers ourselves. Appearance and behavior vary by browser and operating system. Weigh that against building and maintaining a grid widget, and test whichever you choose.
The Calendar Is a Dialog Before It Is a Grid
This is the part our page used to skip entirely. A calendar that opens over the page is a modal dialog, and it owes everything a dialog owes. The container takes role="dialog", aria-modal="true" and an accessible name. Focus moves into it on open, stays inside while it is there, and comes back out when it closes.
Where focus lands on open is specified. It goes to the currently selected date if there is one, and to today if there is not. On close, Escape returns focus to the control that opened the dialog. In W3C's own design that control is a Choose date button sitting beside the input rather than the input itself, so return focus to whatever actually opened it and do not assume it was the field.
There is one more detail in that pattern which is the best idea in the whole subject and takes ten minutes. After somebody picks a date, change the accessible name of the trigger button from Choose date to something like Change date, 15 September 2026. When the dialog closes and focus lands back on the button, the screen reader user hears confirmation of what they just chose without going looking for it.
The Grid Keyboard Model, In Full
Only one date is in the tab order at a time. The focused cell carries tabindex="0" and every other cell carries tabindex="-1", and those values move as focus moves. That technique is called a roving tabindex, and it is the same mechanism behind tab lists, toolbars and data grids.
- Left and Right move by a day. Up and Down move by a week.
- Home and End move within the current week, to its first and last day. Not to the start and end of the month, which is what most people guess and then build.
- Page Up and Page Down move by a month, landing on the same day number. Where that number does not exist, such as the 31st moving into February, focus goes to the last day of the month instead. That edge case is where hand-built pickers break.
- Shift with Page Up or Page Down moves by a year. This is the key that answers the objection at the top of this page, and almost nobody implements it.
- Space or Enter selects the date, closes the dialog, updates the input, and returns focus to the trigger.
Two ARIA details on the cells. Mark the selected date with aria-selected="true", and mark only that one. Unlike a tab list, the other cells carry no aria-selected at all, so a developer coming from tabs who sets false on the rest has just made a screen reader announce forty dates as not selected. Today is a different idea from the selected date, and aria-current="date" is the attribute for it, which is one of the rare cases where current and selected can sit in the same set of elements meaning two different things.
Making a Bare Number Mean Something
A cell reading 15 tells a screen reader user nothing on its own, and there are two ways to fix it that we should not have presented as one.
The first is to give every cell a full accessible name, such as Tuesday 15 September 2026. It works and it is loud. Arrowing across a week means hearing the month and the year on every cell, forty times a month.
The second is what W3C actually does. Build the calendar as a real table carrying role="grid", so the row and column headers are the context, and then put the full day name in an abbr attribute on each column header, because Tu is not a word. A grid announces context when it changes rather than on every cell, which is quieter and closer to how somebody reads a printed calendar. It depends on the grid role being well supported by the reader in question, so test it.
Either way, announce the month when it changes. Putting aria-live="polite" on the heading that says the month and year is W3C's own technique, and without it the next-month button is silent. W3C's example carries a second live region worth stealing, which announces the available keyboard commands shortly after focus enters the grid, slightly delayed so it arrives after the focus announcement rather than on top of it.
For disabled dates, aria-disabled plus a reason is good practice rather than a published requirement, and the greying-out itself is not a contrast finding, because both contrast criteria except inactive components. The argument for making a disabled date legible is usability, not conformance. And if you took the native input, min and max have already done this for you.
Date Ranges
Two fields, two labels, and a clear statement of which is which. A single calendar where the first click sets the start and the second sets the end is efficient with a mouse and close to unusable without one, because nothing tells a non-visual user which half of the range they are choosing. The renaming trick above solves that too. Update the trigger's name to say what has been picked so far and the state stops being invisible.
When a range is invalid, say so in words next to the fields. A red border alone fails on color, carries nothing at all to a screen reader, and leaves out the identification and the suggestion the standard asks for. Say what is wrong and say what would fix it, in one sentence, and you have satisfied both.
Testing It
- Reach the field with the keyboard and type a date. Confirm you never had to open the calendar at all.
- Open the calendar and move to a date three months out using only the arrow keys and Page keys. Then try Shift with Page Up to jump a year, and Home and End to move within the week. A picker that has none of those is the one this page's first section is about.
- Listen to what each date announces. Either a full name such as Tuesday 15 September, or a bare number arriving with its row and column context from a real grid. A bare number with no context at all is the failure.
- Press Escape and confirm focus returns to whatever opened the calendar, then listen to what that control now says. If it still says Choose date, you have skipped the cheapest improvement here.
Before you copy the reference code
W3C's own worked examples carry a warning worth repeating. The code illustrates correct use of the specification and is not intended for production, because it deliberately does not work around gaps in browser and assistive technology support, and those gaps are widest on mobile and touch devices. Build from it. Test what you built.