Accessibility
Built with WCAG 2.2 AA compliance and full keyboard support
ARIA Support
#Timepicker-UI includes ARIA roles, live-region announcements and focus management for screen reader compatibility:
role="dialog"witharia-modal="true"for modal timepickeraria-labelledbyconnecting dialog to titlearia-labelon all interactive elements (inputs, buttons)role="spinbutton"witharia-valuenowfor the hour and minute inputs, and the seconds input whenclock.showSecondsis on (dynamically updated). Witharia-valuetextfromlabels.valueText, a screen reader reads "Hour, 7 of 12" instead of a bare numberrole="group"around the analog clock, named bylabels.clock(default "Clock")role="slider"withtabindex="0"on the clock face. Itsaria-label,aria-valuemin,aria-valuemax,aria-valuenowandaria-valuetextfollow the active hour, minute or second fieldrole="listbox"for the wheel columns (wheel and compact-wheel modes) and the timezone listrole="option"witharia-selectedfor the values in those lists, witharia-activedescendantpointing to the current onearia-disabledon disabled wheel items, on the OK button while the time can't be confirmed (a disabled time, a time that was just cleared, or a range that isn't valid yet), on the Clear button when there is nothing to clear, and on the range To tab until a From time is set. Disabled numbers on the clock face are dimmed, can't be clicked and are skipped by the keyboard. They have noaria-disabledbecause the numbers arearia-hiddenrole="radiogroup"around the AM and PM buttons, named bylabels.period. Each button is arole="radio"witharia-checked(dynamically updated). Before 4.7.0 they usedaria-pressedrole="tablist"with tworole="tab"buttons for the range From and To ends, and arole="tabpanel"(tp-ui-range-panel) around the time fields and the dial. Each tab hasaria-controlspointing to the panel, and the panel is named by the active tab- The selected timezone is a select-only
role="combobox"witharia-expanded,aria-haspopup="listbox"andaria-activedescendant. It is the element with the classtp-ui-timezone-selected role="status"+aria-live="polite"+aria-atomic="true"for time change announcementsaria-hidden="true"for decorative elements (clock numbers, dots, clock hand). The clock numbers are not focusable
Keyboard Navigation
#Complete keyboard support for users who cannot or prefer not to use a mouse:
Keyboard Shortcuts
- Enter Space
- Open timepicker
- Esc
- Close / Cancel (not in inline mode)
- Tab
- Move between controls: hour, minute, second (when shown), AM, PM, clock face, then the footer buttons
- Up arrow Right arrow
- Clock face: one step up (1 hour, or the minute / second increment)
- Down arrow Left arrow
- Clock face: one step down
- PgUp PgDn
- Clock face: ±3 hours, or ±5 steps on minutes and seconds
- Home End
- Clock face: first / last value
- Enter Space
- Clock face: go to the next face (hour, then minute, then second). Off when
clock.autoSwitchToMinutes/autoSwitchToSecondsisfalse - Up arrow Down arrow
- Hour, minute or second field: one step up / down
- Home End
- Hour, minute or second field: first / last value
- PgUp PgDn
- Hour, minute or second field: ±3 hours, or ±5 steps on minutes and seconds
- Enter Space
- Select AM or PM (Tab to the button first)
- Arrow keys
- AM / PM group: move to the other button and select it. The group is one Tab stop, like an APG radio group
- Arrow keys
- Range tabs: move focus between From and To. Enter or Space on the To tab activates it and focus stays on the tab, as in an APG tab list. Tab then moves into the panel
- Enter
- Time field: apply the typed value, like leaving the field
- Letters
- Timezone combobox: jump to the next timezone that starts with the typed letters (type-ahead)
Note: The clock face is a single Tab stop and works like a slider. The arrow keys change the value of the active face (hour, minute or second). The clock hand and the matching field update as you press, and focus stays on the face. Disabled values are skipped on the clock face and in the time fields.
Navigation Flow
- Tab through: Hour input, then Minute input, then Second input (when shown), then AM, then PM, then Clock face, then Cancel, then OK
- On the clock face, press Enter or Space to go to the next face (hour, then minute, then second). Clicking a time field also switches the face
- When the clock face has focus, the arrow keys change the active value and focus stays on the face
- Disabled times (if configured) are skipped during arrow key navigation
- Focus trap prevents tabbing outside the modal while open
Enabling Keyboard Navigation
const picker = new TimepickerUI(input, {// Keyboard navigation is enabled by default// All interactive elements are focusable});picker.create();// Arrow keys work on the time fields and on the clock face// - Hour/minute/second fields: Up/Down step by 1 hour, or by// clock.incrementMinutes / clock.incrementSeconds// - Clock face: Up/Right and Down/Left step the active face the same way// - Disabled times are automatically skipped// AM and PM are buttons: Tab to one and press Enter or Space
Screen Reader Support
#Optimized for screen readers like NVDA, JAWS, and VoiceOver:
ARIA Labels
const picker = new TimepickerUI(input, {labels: {// Visible button text (also announced by screen readers)ok: 'Confirm',cancel: 'Cancel',// Accessible names for the picker controlsclock: 'Clock',period: 'Period',timeFields: 'Select appointment time',// Field caption, accessible name and announcement prefix in one keyhour: 'Hour',minute: 'Minute'}});// Add a custom aria-label / hint directly on the input element<inputtype="text"id="timepicker"aria-label="Select appointment time"aria-describedby="time-help"/><span id="time-help" class="sr-only">Press Enter or Space to open the time picker.</span>
Live Regions
<!-- Live region for screen reader announcements --><!-- Rendered as the last child of .tp-ui-modal; sr-only hides it visually --><divclass="timepicker-announcer sr-only"role="status"aria-live="polite"aria-atomic="true"><!-- Dynamically updated by the timepicker --><!-- Announces: "Hour: 14", "Minute: 30", etc. --></div><script>// Timepicker automatically announces changes via live region// No manual setup required// Example announcements with the default labels:// - "Hour: 14"// - "Minute: 30" (said "Minutes: 30" before 4.7.0)// - "Second: 15"// - "AM selected"// - "PM selected"// - "Time cleared"// Live region updates happen on:// - Clock hand drag// - Clock number click// - Arrow keys, Home/End and PageUp/PageDown on the time fields// - Enter/Space on the clock face (announces the new face and its value)// - Wheel column scroll (wheel and compact-wheel modes)// - AM/PM toggle// - Clear// Arrow steps on the focused clock face are not announced here: the// clock face is a slider, so the screen reader reads its new value itself</script>
Descriptive Labels
<label for="meeting-time" class="form-label">Meeting Time<span class="text-muted">(Required)</span></label><inputtype="text"id="meeting-time"requiredaria-required="true"aria-describedby="meeting-time-hint"/><div id="meeting-time-hint" class="form-hint">Select a time between 9:00 AM and 5:00 PM</div>
Value text and sizes
#The hour, minute and second fields and the clock dial expose their value through aria-valuetext. The text comes from labels.valueText, "{value} of {count}" by default, where {value} has no leading zero and {count} is 12 or 24 hours, or 60 minutes or seconds. With the field name a screen reader reads "Hour, 7 of 12". Set '{value}' for the bare number. New in 4.7.0.
For Material 3 target sizes, set these variables. The hand circle grows to 48 px, and the AM / PM buttons get 48 px with a taller time field next to them.
.tp-ui-wrapper {--tp-size-hand-circle: 48px;--tp-size-input-height: 96px;--tp-size-am-pm-height: 48px;--tp-size-am-pm-height-landscape: 48px;}
Focus Management
#Proper focus handling for keyboard and screen reader users:
Focus Trapping
// Focus is automatically trapped within the modal// Users cannot tab outside the timepicker when openconst picker = new TimepickerUI(input, {behavior: {// Modal automatically manages focusfocusTrap: true, // Default behaviorreturnFocus: true // Recommended, default: false (true in 5.0)}});picker.create();// When modal opens:// 1. Focus moves to the wrapper element// 2. Tab cycles through all focusable elements in order:// - Hour input// - Minute input// - Second input (when clock.showSeconds is true)// - AM/PM radio group, one stop on the checked one (in 12h mode)// - Clock face (one Tab stop for the whole dial)// - Cancel button// - OK button// 3. Shift+Tab cycles backwards// 4. When last element is focused and Tab is pressed, focus returns to first element// 5. Cannot tab to elements behind modal// When modal closes:// - Focus returns to the input field that triggered the modal// when behavior.returnFocus is true (default: false)// Note: The numbers on the clock face are not in the Tab order. The clock// face itself works like a slider: the arrow keys change the active value.
Visible Focus Indicators
/* Built-in focus ring (simplified) */.tp-ui-ok-btn:focus-visible,.tp-ui-cancel-btn:focus-visible,.tp-ui-clear-btn:focus-visible,.tp-ui-keyboard-icon:focus-visible {outline: 3px solid var(--tp-focus-ring, var(--tp-primary));outline-offset: 2px;box-shadow: var(--tp-shadow-focus-ring, none);}/* AM/PM, the time fields and the clock face draw the ring inside their edge */.tp-ui-hour:focus-visible,.tp-ui-minutes:focus-visible,.tp-ui-seconds:focus-visible,.tp-ui-am:focus-visible,.tp-ui-pm:focus-visible,.tp-ui-clock-face:focus-visible {outline-offset: -3px;}/* Thicker ring when the user asks for more contrast */@media (prefers-contrast: more) {.tp-ui-ok-btn:focus-visible,.tp-ui-cancel-btn:focus-visible {outline-width: 4px;}}/* The ring shows only for keyboard focus, not after a mouse click or tap *//* Set --tp-focus-ring to change its color */
Disabled Time Handling
// Disabled times are properly marked and skipped// Lists: disable specific hours / minutesconst picker = new TimepickerUI(input, {clock: {type: '24h',disabledTime: {hours: [0, 1, 2],minutes: [15, 45]}}});// Ranges: disable one or more time ranges (string or array of strings).// When ranges is set, the hours, minutes and seconds lists are ignoredconst rangesPicker = new TimepickerUI(input, {clock: {disabledTime: {ranges: ['9:00 AM - 12:00 PM', '6:00 PM - 11:59 PM']}}});// On the clock face, disabled numbers:// - are dimmed and can't be picked by click or drag// - are skipped by the arrow keys, Home/End and PageUp/PageDown// - have no aria-disabled, because the numbers are aria-hidden and not// focusable (the clock face itself is the focusable slider)// In wheel and compact-wheel modes, disabled items get aria-disabled="true",// or are left out with wheel.hideDisabled: true// OK gets aria-disabled="true" while the shown time is disabled// The keyboard skips disabled values in the time fields too// If the ranges cover every minute of an hour, that hour is also skipped
Color Contrast
#Ensure WCAG AA compliance with proper color contrast:
Contrast Requirements
- WCAG AA:
- Text contrast ratio of at least 4.5:1
- WCAG AAA:
- Text contrast ratio of at least 7:1
- Large Text:
- 18px or 14px bold requires 3:1 contrast
High Contrast Theme
/* High contrast accessible override (CSS variables use the --tp- prefix) */.tp-ui-modal[data-theme="dark"] {--tp-bg: #000000;--tp-text: #ffffff;--tp-primary: #ffff00;--tp-border: #ffffff;/* Ensure all text meets 7:1 contrast ratio */}/* Respect user's contrast preference */@media (prefers-contrast: more) {.tp-ui-wrapper {border: 2px solid currentColor;}.tp-ui-clock-face {border: 2px solid currentColor;}}@media (prefers-contrast: more) {/* Higher contrast for users who ask for it */.tp-ui-wrapper {background: #000000;color: #ffffff;}}
Reduced Motion
#Respect user preferences for reduced motion:
Disable Animations
/* CSS: Respect prefers-reduced-motion */@media (prefers-reduced-motion: reduce) {.tp-ui-wrapper {animation: none !important;transition: none !important;}.tp-ui-clock-hand {transition: none !important;}.tp-ui-modal {animation: none !important;}}// JavaScript: Detect user preferenceconst prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;const picker = new TimepickerUI(input, {ui: {animation: !prefersReducedMotion}});picker.create();
Testing Accessibility
#Tools and techniques for testing accessibility:
Automated Testing
// Using axe-core for accessibility testingimport { axe } from 'jest-axe';test('timepicker should have no accessibility violations', async () => {const { container } = render(<TimepickerComponent />);const results = await axe(container);expect(results).toHaveNoViolations();});// Using @testing-library for keyboard testingimport { render, screen } from '@testing-library/react';import userEvent from '@testing-library/user-event';test('can navigate with keyboard', async () => {const user = userEvent.setup();render(<TimepickerComponent />);const input = screen.getByRole('textbox');await user.click(input);await user.keyboard('{Enter}');// Verify modal openedexpect(screen.getByRole('dialog')).toBeInTheDocument();// Test arrow key navigationawait user.keyboard('{ArrowUp}');await user.keyboard('{Enter}');});
Manual Testing Checklist
- Accessibility Resources
- Learn more about web accessibility standards and best practices: