Date Picker
A date picker lets the user choose a date by typing it or by picking it from a
calendar. Both paths stay open at all times — the text field takes a
DD/MM/YYYY value directly, and the trailing button opens a calendar beside it.
The styleguide ships the calendar itself (.tng-calendar); the field around it
is Field and the surface it opens into is
Dropdown.
March 2026
Ready to use
A Date Picker is a form input component that lets users enter or select a date. It combines a text field (for direct entry) with a calendar popup (for visual selection), so users can complete the task with either keyboard-first or pointer-first behaviour. The component is fully responsive and designed to be used across mobile, tablet, and desktop without separate variants.
Anatomy
Section titled “Anatomy”March 2026
- Calendar
- Header
- Navigation Button
- Weekdays
- Selected day
- Today
- Unavailable day
When to use it
Section titled “When to use it”Use the Date Picker when:
- The user needs to select a specific calendar date (for example, a preferred test-drive day).
- The date is in the near future or recent past and calendar context improves selection confidence.
- Input must be constrained to a valid date range (min/max dates).
- Date format consistency is required for downstream processing.
Avoid the Date Picker when:
- Only a month or a year is required.
- The interaction is relative rather than absolute (for example, “in X days”).
- There is no persistent, visible label associated with the field.
Properties
Section titled “Properties”State
Rested, Hover, Active, Disabled, Focus, and Error. Active opens the calendar while keeping typed input available; Focus highlights the currently edited field for keyboard users without opening the popup; Error appears after blur when the value is invalid.
Required
A boolean that marks the field as mandatory. The label reflects the required state visually and through the form submission semantics.
Helper text
Optional text beneath the field — use it to show the expected date format with a concrete example. The component swaps in an error message in the same slot when validation fails.
Trailing icon button
A trailing icon button doubles as the trigger that opens the popup. Figma draws it as a chevron — pointing down while the popup is closed, up while it is open — and swaps in the calendar glyph for the error and required-error states. Whichever glyph it carries, the button needs its own accessible name; the icon is decorative.
Calendar popup
The month grid that opens on click or tap. Day cells expose full dates via aria-label, disabled or unavailable days stay visible but non-selectable, the selected date is filled, and the current date carries a distinct outline. Figma has no frame for this popup — it is documented on the CSS and Accessibility tabs.
Platform considerations
Section titled “Platform considerations”Desktop
The calendar opens beneath the input, flipping above it when there isn’t room. Keyboard interaction covers the field, the trigger, month navigation, and day cells — focus moves into the popup when it opens and comes back out to the field or the trigger when it closes. See the Accessibility tab for the exact sequence.
Tablet
Same behaviour as desktop. Make sure the popup has enough room to render below the input without clipping; flip above when needed.
Mobile
Keep both paths available — typed input shouldn’t disappear in favour of calendar-only on smaller screens. The calendar popup may shift to a sheet pattern but still respects focus and aria-label semantics.
Best practices
Section titled “Best practices”Treat the picker as a dual-path control — typing and tapping should both work all the way through.
Do
Keep both interaction paths available (typed input and calendar selection), validate on blur and provide a clear error message that includes the expected format, use helper text to show a concrete example date, and keep disabled and unavailable dates visible but non-selectable inside the calendar.
Don't
Don’t hide or replace the persistent label with placeholder-only text, don’t rely on colour alone for the error or selected state (pair it with an icon or copy), don’t force calendar-only interaction for keyboard and assistive-tech users, and don’t trigger validation errors while users are still typing.
Content guidelines
Section titled “Content guidelines”Make the label specific to the date you’re asking for (“Preferred test-drive date”, not just “Date”). Use the helper text to demonstrate the expected format with a real example (“DD/MM/YYYY — 25/12/2026”) so the user can match the pattern before the picker opens. Error copy should name the rule that failed (“Pick a date after today”) rather than restating the field — pair it with the error icon so the message reads without colour alone.
The styleguide ships the calendar, not the whole picker. .tng-calendar is a
seven-column grid; the trigger field around it is
Field and the surface it opens into is
Dropdown. Compose the three yourself — the Recipe
below shows the finished result.
.tng-calendar-header, .tng-calendar-weekdays and each .tng-calendar-week
span all seven columns and pick the tracks up again through subgrid. The ARIA
grid wrappers (role="grid" on the month, role="gridcell" on each day) are
display: contents, so the semantics cost nothing in layout — keep them.
Day cells are plain <button> elements. Four looks ship: the default,
.is-selected for the chosen date, .is-today for the current date, and native
disabled for days outside the month or outside the allowed range. Selection
uses .is-selected rather than [aria-selected] because that is what the CSS
targets today — set both, as the snippet does, so the visual and the semantic
stay in step.
<div class="tng-calendar"> <div class="tng-calendar-header"> <button class="tng-icon-button is-ghost" type="button" aria-label="Previous month" > <i class="tng-icon icon-chevron-left" aria-hidden="true"></i> </button> <h2 class="tng-text-body" id="calendar-month" aria-live="polite"> March 2026 </h2> <button class="tng-icon-button is-ghost" type="button" aria-label="Next month" > <i class="tng-icon icon-chevron-right" aria-hidden="true"></i> </button> </div> <div role="grid" aria-labelledby="calendar-month"> <div class="tng-calendar-weekdays" role="row"> <div role="columnheader" aria-label="Monday">Mon</div> <div role="columnheader" aria-label="Tuesday">Tue</div> <div role="columnheader" aria-label="Wednesday">Wed</div> <div role="columnheader" aria-label="Thursday">Thu</div> <div role="columnheader" aria-label="Friday">Fri</div> <div role="columnheader" aria-label="Saturday">Sat</div> <div role="columnheader" aria-label="Sunday">Sun</div> </div> <div class="tng-calendar-week" role="row"> <div role="gridcell"> <button type="button" disabled aria-label="February 23, 2026"> 23 </button> </div> <div role="gridcell"> <button type="button" disabled aria-label="February 24, 2026"> 24 </button> </div> <div role="gridcell"> <button type="button" disabled aria-label="February 25, 2026"> 25 </button> </div> <div role="gridcell"> <button type="button" disabled aria-label="February 26, 2026"> 26 </button> </div> <div role="gridcell"> <button type="button" disabled aria-label="February 27, 2026"> 27 </button> </div> <div role="gridcell"> <button type="button" disabled aria-label="February 28, 2026"> 28 </button> </div> <div role="gridcell"> <button type="button" aria-label="March 1, 2026">1</button> </div> </div> <div class="tng-calendar-week" role="row"> <div role="gridcell"> <button type="button" aria-label="March 9, 2026">9</button> </div> <div role="gridcell"> <button type="button" aria-label="March 10, 2026">10</button> </div> <div role="gridcell"> <button type="button" class="is-selected" aria-selected="true" aria-label="March 11, 2026" > 11 </button> </div> <div role="gridcell"> <button type="button" aria-label="March 12, 2026">12</button> </div> <div role="gridcell"> <button type="button" aria-label="March 13, 2026">13</button> </div> <div role="gridcell"> <button type="button" aria-label="March 14, 2026">14</button> </div> <div role="gridcell"> <button type="button" aria-label="March 15, 2026">15</button> </div> </div> <div class="tng-calendar-week" role="row"> <div role="gridcell"> <button type="button" aria-label="March 23, 2026">23</button> </div> <div role="gridcell"> <button type="button" aria-label="March 24, 2026">24</button> </div> <div role="gridcell"> <button type="button" aria-label="March 25, 2026">25</button> </div> <div role="gridcell"> <button type="button" class="is-today" aria-current="date" aria-label="March 26, 2026" > 26 </button> </div> <div role="gridcell"> <button type="button" aria-label="March 27, 2026">27</button> </div> <div role="gridcell"> <button type="button" aria-label="March 28, 2026">28</button> </div> <div role="gridcell"> <button type="button" aria-label="March 29, 2026">29</button> </div> </div> </div></div>March 2026
.tng-calendar has no width of its own — it fills whatever box it is placed in,
and aspect-ratio: 1 on the day cells means the calendar grows as tall as it is
wide. Constrain the surface it sits in (Figma caps the picker at 400px) rather
than the calendar itself.
Recipes
Section titled “Recipes”March 2026
The date picker follows the WAI-ARIA Date Picker Dialog pattern — a text field paired with a calendar the user opens in a dialog — and meets WCAG 2.1 AA. An accessible date picker keeps typed entry available alongside the calendar, moves and returns focus predictably as the dialog opens and closes, announces the month as it changes, and gives every element an accessible name — so the chosen date is clear whether the user sees it, hears it, or tabs through it.
For designers
Section titled “For designers”- Keep the label visible and outside the field in every state, and keep the format hint (for example,
DD/MM/YYYY) in helper text at all times — never let the placeholder carry the label or the format. - The field, its text, the helper and error text, the calendar trigger, and every day cell must maintain accessible contrast on every supported surface and in every state. Source: WCAG 1.4.3 Contrast (Minimum).
- The selected, today, error, and disabled states must never be conveyed by colour alone — pair each with an icon, a shape, or text. Source: WCAG 1.4.1 Use of Color.
- Focus styles must stay visible across the field, trigger, month controls, and day cells, meet contrast requirements on every supported surface, and be clearly distinguishable from hover — including high-contrast mode. Source: WCAG 2.4.7 Focus Visible.
- Use semantic colour tokens (e.g.
foreground/neutral/default) rather than fixed values, so the component adapts correctly across themes and surfaces.
For developers
Section titled “For developers”The date picker combines a native text field with a non-modal dialog that holds a calendar grid. Typed entry must always stay available — users must be able to complete the task without traversing day-by-day cells, and typing a date must land in the same place as picking one, so a complete value re-syncs the calendar’s selection and a keystroke in the field gets the calendar out of the way. The calendar itself is a single tab stop: a roving tabindex moves focus between days, and the arrow keys drive it — the traversal is in Focus order below and the keys are in the Keyboard interaction table. Interactive targets must be at least 44 px high for comfortable touch use — comfortably clearing the WCAG AA minimum. Source: WCAG 2.5.8 Target Size (Minimum). The per-element roles, names, and states — the field, trigger, dialog, grid, and day cells — are in the Labelling elements section below.
Keyboard interaction
Section titled “Keyboard interaction”| Key | Action |
|---|---|
| Enter / Space | Open the picker from the trigger, or select the focused day — selecting closes the picker and returns focus to the field |
| Arrow keys | Move focus between days in the grid, wrapping at the edges; ↓ also opens the picker from the field |
| Home / End | Move focus to the first / last day of the grid |
| Escape | Close the picker and return focus to the trigger |
| Tab / Shift + Tab | Move through the field, trigger, and the controls inside the open picker; Shift + Tab off the first control closes it and returns to the field |
| Printable characters | Typing in the field closes the picker, so typed entry never fights the calendar for focus |
Focus order
Section titled “Focus order”Focus reaches the field and its trigger; opening the picker moves focus into the dialog and onto the selected day. Tab then walks the month controls and the calendar grid, which is a single stop — a roving tabindex moves focus between days while the arrow keys rove within it. Focus stays inside the open picker, with two deliberate exits: Escape closes it and returns focus to the trigger, while selecting a day closes it and returns focus to the field, so the user lands on the value they just chose. The keys that drive this are in the Keyboard interaction table above.
March 2026
- Date field
- Calendar trigger
- Previous month
- Next month
- Calendar grid
Source: WCAG 2.4.3 Focus Order.
Labelling elements
Section titled “Labelling elements”Give every element the role, name, and state assistive technology needs.
Date field
A native <input type="text"> associated with its <label> through for/id. Link the format hint and any error text with aria-describedby, and announce error changes with aria-live="polite". Set inputmode="numeric" so mobile surfaces the numeric keyboard.
Trigger
The calendar <button> carries aria-haspopup="dialog", aria-expanded kept in sync with the open state, and aria-controls pointing to the dialog. Give it an accessible name with aria-label (e.g. "Choose date"); its icon is aria-hidden="true".
Dialog
The popup surface takes role="dialog" and is named with aria-label or aria-labelledby. It is non-modal (aria-modal="false"), so the underlying page stays reachable and readable — the picker holds focus while it is open, but by moving it back on close rather than by making the rest of the page inert.
Calendar grid
role="grid" with aria-labelledby pointing to the month/year heading. It wraps the weekday strip and the weeks — put it on its own element inside .tng-calendar rather than on .tng-calendar itself; the styleguide makes it display: contents so the grid tracks pass straight through. The weekday strip is a role="row" of role="columnheader" cells, each named with the full weekday (aria-label="Monday", not Mon), and every week is another role="row" — so screen readers announce the day’s column and position.
Month & year heading
The visible <h2> names the grid via aria-labelledby and carries aria-live="polite", so the new month is announced when the user steps between months.
Month navigation buttons
Each icon-only <button> takes its accessible name from aria-label ("Previous month" / "Next month"), with the chevron marked aria-hidden="true".
Day cell
A role="gridcell" wrapper around a native <button>. Expose the full date as the button’s accessible name (aria-label="March 11, 2026") — the visible 11 alone is meaningless out of context — and name the unavailable days too. Set aria-selected="true" on the chosen date and aria-current="date" on today, and keep unavailable days as native disabled so they stay visible but out of the tab sequence. The styleguide paints those two states from .is-selected and .is-today, not from the ARIA, so set both — the class for the look, the attribute for the announcement.
Source: WCAG 4.1.2 Name, Role, Value.