A character counter that avoids the two most common accessibility failures of this pattern: saying nothing to screen reader users at all, or announcing every single keystroke. Instead, updates are debounced during normal typing, with specific milestones (20, 10, 5, 1 characters remaining), crossing the limit, and reaching it exactly, all interrupting the debounce to announce immediately.
This also correctly suppresses announcements during IME composition (used for languages like Chinese, Japanese, and Korean), so intermediate, not-yet-committed characters aren't announced mid-composition.
Expected result: the visible counter should update on every keystroke. The announced value should update at the milestones described above, not on every keystroke, and should announce immediately when crossing into or out of the limit, or reaching it exactly.
<label for="comment">Add a comment</label>
<span id="comment-help">Enter up to 100 characters</span>
<textarea
id="comment"
aria-describedby="comment-help"
data-maxlength="100"
></textarea>
<span id="comment-count-visible" aria-hidden="true"></span>
<span id="comment-count-live" aria-live="polite" aria-atomic="true"></span>
Worth testing directly: the script uses requestAnimationFrame to briefly clear the live region before setting new text, this forces a re-announcement even when the new message is identical to the previous one, since some screen reader/browser combinations only announce on an actual text change. A short setTimeout (commonly 100–150ms) is a more established version of this same technique elsewhere in the accessibility community. Whether requestAnimationFrame's much shorter delay (typically under 20ms, the time to the next paint) is reliably long enough for every AT/browser combination to register two distinct mutations, rather than coalescing them into one, hasn't been verified here.
The two "Demo link" anchors above and below the counter are included to test that tab order and focus flow sensibly around the live region, and that the region itself never unexpectedly receives or traps focus.