timer
A type of live region containing a numerical counter which indicates an amount of elapsed time from a start point, or remaining time until an end point.
Overview
The timer role indicates a numerical counter for elapsed or remaining time. By default, timer updates are not announced (aria-live="off") to prevent overwhelming users with constant updates.
You can selectively enable announcements at important milestones by temporarily changing aria-live to "polite" or "assertive" when needed.
Silent by Default
Imagine if every second of a countdown was announced - it would be unbearable! That's why timers have aria-live="off" by default. Only announce at meaningful moments (1 minute left, 10 seconds left, etc.).
Live Demo: Countdown Timer
Note: Timer updates are NOT announced by default. In production, you would programmatically enable announcements at key moments (e.g., "1 minute remaining", "10 seconds remaining").
Code Examples
Basic Timer
<!-- Basic Timer -->
<div role="timer" aria-label="Session Timer">
<span id="minutes">05</span>:<span id="seconds">00</span>
</div>
<!-- Note: By default, timer updates are NOT announced (aria-live="off") -->
<!-- Only announce at significant milestones -->Countdown with Milestones
<!-- Countdown Timer with Announcements -->
<div id="countdown"
role="timer"
aria-live="off"
aria-label="Quiz Time Remaining">
<strong id="time-display">10:00</strong>
</div>
<script>
let timeLeft = 600; // 10 minutes in seconds
const timer = setInterval(() => {
timeLeft--;
const minutes = Math.floor(timeLeft / 60);
const seconds = timeLeft % 60;
document.getElementById('time-display').textContent =
`${minutes}:${seconds.toString().padStart(2, '0')}`;
// Announce at important milestones
const timerEl = document.getElementById('countdown');
if (timeLeft === 60) {
timerEl.setAttribute('aria-live', 'polite');
timerEl.setAttribute('aria-atomic', 'true');
// Will announce: "Quiz Time Remaining: 1:00"
setTimeout(() => timerEl.setAttribute('aria-live', 'off'), 1000);
}
if (timeLeft === 0) {
clearInterval(timer);
timerEl.setAttribute('aria-live', 'assertive');
timerEl.textContent = 'Time is up!';
}
}, 1000);
</script>Stopwatch
<!-- Stopwatch / Elapsed Time -->
<div role="timer" aria-label="Elapsed Time">
<span id="elapsed">00:00:00</span>
</div>
<button id="start">Start</button>
<button id="stop">Stop</button>
<button id="reset">Reset</button>
<script>
let startTime;
let interval;
function updateDisplay() {
const elapsed = Date.now() - startTime;
const hours = Math.floor(elapsed / 3600000);
const minutes = Math.floor((elapsed % 3600000) / 60000);
const seconds = Math.floor((elapsed % 60000) / 1000);
document.getElementById('elapsed').textContent =
`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
document.getElementById('start').addEventListener('click', () => {
startTime = Date.now();
interval = setInterval(updateDisplay, 100);
});
document.getElementById('stop').addEventListener('click', () => {
clearInterval(interval);
});
</script>React Component
// React Timer Component
import { useState, useEffect } from 'react';
function CountdownTimer({ initialSeconds, onComplete }) {
const [secondsLeft, setSecondsLeft] = useState(initialSeconds);
const [isActive, setIsActive] = useState(false);
useEffect(() => {
let interval = null;
if (isActive && secondsLeft > 0) {
interval = setInterval(() => {
setSecondsLeft(seconds => seconds - 1);
}, 1000);
} else if (secondsLeft === 0) {
onComplete?.();
setIsActive(false);
}
return () => {
if (interval) clearInterval(interval);
};
}, [isActive, secondsLeft, onComplete]);
const minutes = Math.floor(secondsLeft / 60);
const seconds = secondsLeft % 60;
// Announce at key moments
const shouldAnnounce = secondsLeft === 60 || secondsLeft === 30 || secondsLeft === 10;
return (
<div>
<div
role="timer"
aria-live={shouldAnnounce ? 'polite' : 'off'}
aria-atomic="true"
aria-label="Time Remaining"
>
{minutes}:{seconds.toString().padStart(2, '0')}
</div>
<button onClick={() => setIsActive(!isActive)}>
{isActive ? 'Pause' : 'Start'}
</button>
<button onClick={() => setSecondsLeft(initialSeconds)}>
Reset
</button>
</div>
);
}
// Stopwatch Example
function Stopwatch() {
const [elapsedMs, setElapsedMs] = useState(0);
const [isRunning, setIsRunning] = useState(false);
const [startTime, setStartTime] = useState(0);
useEffect(() => {
let interval;
if (isRunning) {
interval = setInterval(() => {
setElapsedMs(Date.now() - startTime);
}, 10);
}
return () => clearInterval(interval);
}, [isRunning, startTime]);
const hours = Math.floor(elapsedMs / 3600000);
const minutes = Math.floor((elapsedMs % 3600000) / 60000);
const seconds = Math.floor((elapsedMs % 60000) / 1000);
return (
<div
role="timer"
aria-label="Stopwatch"
aria-live="off"
>
{hours.toString().padStart(2, '0')}:
{minutes.toString().padStart(2, '0')}:
{seconds.toString().padStart(2, '0')}
</div>
);
}Best Practices
Keep aria-live="off" for continuous updates
Announce only at significant milestones (60s, 30s, 10s remaining)
Use aria-live="assertive" when timer reaches zero for urgent tasks
Provide clear aria-label (e.g., "Quiz Time Remaining")
Include time units in announcements for clarity
Don't announce every second - it's extremely annoying
Don't use for decorative animated counters
Don't forget to announce when timer completes