Code examples

Use semantic HTML

  • There are two containers in the HTML that have the same counter content. One of them is hidden from screen readers by use of aria-hidden="true" and the other visually hidden container’s content is dynamically updated after a slight pause. This is to ensure the screen reader does not interrupt the announcement of the key pressed with the announcement of the dynamic counter text update.
  • While the visible counter text container is hidden with aria-hidden="true" it is still programmatically associated with the textarea by use of aria-describedby. This will ensure the text will be announced when the textarea receives focus.
  • Delay the update for dynamic role="status" counter
    • Use setTimeoutto allow the accessibility tree and screen reader time to update in a logical fashion e.g. 1500ms
  • Do not reference the role="status" element with aria-describedby
    • This causes a bug in VoiceOver
const textarea = document.getElementById('message');
if(textarea) {
    const chars = document.getElementById('currentChars');
    const srOutputTarget = document.getElementById('sr-counter-target');
    textarea.addEventListener("input", event => {
        const target = event.currentTarget;
        const maxLength = target.getAttribute("maxlength");
        const currentLength = target.value.length;
        // update the visible counter text
        chars.innerHTML = maxLength - currentLength;
        // update the visually hidden counter text
        setTimeout(function() {
            srOutputTarget.innerHTML = maxLength - currentLength;
<label for="message">
  Your message
<!-- Do not reference the status element with aria-describedby 
      Doing so will not work in VoiceOver -->
<div id="charcounter" class="hint" aria-hidden="true">
  <span id="currentChars">50</span> of 50 <span class="hidden">characters remaining</span>
<div class="hidden" id="sr-counter-output-wrapper" role="status">
  <span id="sr-counter-target">50</span> of 50</span> characters remaining