Accessibility

Built with WCAG 2.1 compliance and full keyboard support

ARIA Support

Timepicker-UI includes comprehensive ARIA attributes for screen reader compatibility:

  • role="dialog" with aria-modal="true" for modal timepicker
  • aria-labelledby connecting dialog to title
  • aria-label on all interactive elements (inputs, buttons)
  • role="spinbutton" with aria-valuenow for hour/minute inputs (dynamically updated)
  • role="listbox" for clock face wrappers
  • role="option" with aria-selected for active time values on clock
  • aria-disabled for disabled times (respects disabledTime configuration)
  • aria-pressed for AM/PM toggle buttons (dynamically updated)
  • role="status" + aria-live="polite" + aria-atomic="true" for time change announcements
  • aria-hidden="true" for decorative elements (dots, clock hand)

Keyboard Navigation

Complete keyboard support for users who cannot or prefer not to use a mouse:

Keyboard Shortcuts

Open timepickerEnter
Close / CancelEsc
Navigate between elementsTab
Adjust time on clock face (±1 min/hour)
Adjust time on hour/minute inputs (±1)
Switch view (hour ↔ minute) from inputEnter
Quick toggle to AMA
Quick toggle to PMP

Note: Clock face numbers are Tab-focusable. Use arrow keys while focused to adjust time by 1 minute/hour. The clock hand and input values update in real-time, but focus stays on the current element for easier fine-tuning.

Navigation Flow

1.Tab through: Hour input Minute input Clock face numbers AM/PM Cancel OK
2.When focused on hour/minute input, press Enter to switch clock face view
3.When focused on clock face numbers, use arrow keys to adjust time without moving focus
4.Disabled times (if configured) are skipped during arrow key navigation
5.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 both inputs and clock face
// - On hour/minute inputs: Up/Down adjusts by ±1
// - On clock face numbers: Up/Down/Left/Right adjusts by ±1
// - Disabled times are automatically skipped
// A/P shortcuts work from anywhere except inputs
document.addEventListener('keydown', (e) => {
if (e.key === 'a' || e.key === 'A') {
// Switches to AM (when not typing in input)
}
if (e.key === 'p' || e.key === 'P') {
// Switches to PM (when not typing in input)
}
});

Screen Reader Support

Optimized for screen readers like NVDA, JAWS, and VoiceOver:

ARIA Labels

const picker = new TimepickerUI(input, {
// Labels are announced by screen readers
okLabel: 'Confirm time selection',
cancelLabel: 'Cancel time selection',
// Add custom aria-label to input
ariaLabel: 'Select appointment time'
});
// Or add to HTML
<input
type="time"
id="timepicker"
aria-label="Select appointment time"
aria-describedby="time-help"
/>
<span id="time-help" class="sr-only">
Use arrow keys to select time. Press Enter to confirm.
</span>

Live Regions

<!-- Live region for screen reader announcements -->
<div
class="timepicker-announcer"
role="status"
aria-live="polite"
aria-atomic="true"
style="position: absolute; width: 1px; height: 1px;
overflow: hidden; clip: rect(0,0,0,0);"
>
<!-- Dynamically updated by the timepicker -->
<!-- Announces: "Hour changed to 14", "Minute changed to 30", etc. -->
</div>
<script>
// Timepicker automatically announces changes via live region
// No manual setup required - works out of the box
// Example announcements:
// - "Hour changed to 14"
// - "Minute changed to 30"
// - "AM selected"
// - "PM selected"
// Live region updates happen on:
// - Clock hand drag
// - Clock number click
// - Arrow key navigation
// - AM/PM toggle
</script>

Descriptive Labels

<label for="meeting-time" class="form-label">
Meeting Time
<span class="text-muted">(Required)</span>
</label>
<input
type="time"
id="meeting-time"
required
aria-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>

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 open
const picker = new TimepickerUI(input, {
// Modal automatically manages focus
focusTrap: true // Default behavior
});
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
// - Clock face numbers (all visible hour/minute values)
// - AM/PM buttons (in 12h mode)
// - 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
// Note: Clock face numbers are included in Tab order, allowing keyboard-only
// users to reach them. Once focused, arrow keys can adjust time precisely.

Visible Focus Indicators

/* Default focus styles for all interactive elements */
.timepicker-ui-hour-time-12:focus-visible,
.timepicker-ui-minutes-time:focus-visible,
.timepicker-ui-hour-time-24:focus-visible,
.timepicker-ui-am:focus-visible,
.timepicker-ui-pm:focus-visible,
.timepicker-ui-cancel-btn:focus-visible,
.timepicker-ui-ok-btn:focus-visible,
.timepicker-ui-keyboard-icon-wrapper:focus-visible {
outline: 3px solid var(--timepicker-primary);
outline-offset: 2px;
box-shadow: 0 0 0 4px rgba(98, 0, 238, 0.2);
}
/* High contrast mode support */
@media (prefers-contrast: high) {
.timepicker-ui-hour-time-12:focus-visible,
.timepicker-ui-minutes-time:focus-visible,
.timepicker-ui-hour-time-24:focus-visible,
.timepicker-ui-am:focus-visible,
.timepicker-ui-pm:focus-visible,
.timepicker-ui-cancel-btn:focus-visible,
.timepicker-ui-ok-btn:focus-visible {
outline-width: 4px;
font-weight: 700;
}
}
/* Focus styles use :focus-visible to only show for keyboard navigation */
/* Mouse clicks won't show focus ring, but Tab navigation will */

Disabled Time Handling

// Disabled times are properly marked and skipped
const picker = new TimepickerUI(input, {
disabledTime: {
interval: true,
intervals: [
['9:00 AM', '12:00 PM'], // Disabled range
['6:00 PM', '11:59 PM'] // Another disabled range
]
}
});
// Disabled times have:
// - tabIndex = -1 (not focusable via Tab)
// - aria-disabled="true" (announced by screen readers)
// - Visual styling (grayed out)
// - Skipped during arrow key navigation
// Arrow key navigation automatically skips disabled values
// If all minutes in an hour are disabled, 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 theme */
[data-theme="high-contrast"] {
--timepicker-bg-color: #000000;
--timepicker-text-color: #ffffff;
--timepicker-primary-color: #ffff00;
--timepicker-border-color: #ffffff;
/* Ensure all text meets 7:1 contrast ratio */
}
/* Respect user's contrast preference */
@media (prefers-contrast: high) {
.timepicker-ui-wrapper {
border: 2px solid currentColor;
}
.timepicker-ui-clock-face__clock-number {
font-weight: 700;
}
}
@media (prefers-contrast: more) {
/* Enhanced contrast for users who need it */
.timepicker-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) {
.timepicker-ui-wrapper {
animation: none !important;
transition: none !important;
}
.timepicker-ui-clock-face__clock-hand {
transition: none !important;
}
.timepicker-ui-backdrop {
animation: none !important;
}
}
// JavaScript: Detect user preference
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
const picker = new TimepickerUI(input, {
animation: !prefersReducedMotion
});
picker.create();

Testing Accessibility

Tools and techniques for testing accessibility:

Automated Testing

// Using axe-core for accessibility testing
import { 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 testing
import { 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 opened
expect(screen.getByRole('dialog')).toBeInTheDocument();
// Test arrow key navigation
await user.keyboard('{ArrowUp}');
await user.keyboard('{Enter}');
});

Manual Testing Checklist

Accessibility Resources

Learn more about web accessibility standards and best practices: