Preface
When auditing forms, I've often noticed that it was particularly difficult to keep track of the numbers of characters remained in text field with an imposed limit. While I could approximate based on visual clues, the practice was virtually impossible when using a screen reader.
The following script dynamically counts and displays the number of characters remaining in an input based on that field's maxlength
value. The script also provides a secondary counter on a delay for screen readers. It's goal is to update the user when they pause for a preset amount of time.
Demo
As you type in the following field, notice how the first counter updates dynamically as you type whereas the second only updates after two seconds of inactivity. The second counter is normally hidden visually but is left on for this demo.
Preview
How it works
The script binds itself to any input with the .counted
class and looks for the maxlength
attribute. Based on this input's value, it populates the sibling .screen-only
and .sr-only
elements.
As the user types in the field, the number of characters remaining is counted based on the maxlegnth
and this value is dynamically appended to the .screen-only
element. After two second of inactivity, the .sr-only
element is updated.
Solution
HTML
To function, the script relies on elements with specific class names and attributes: an input or textarea with the .counted
class and a maxlength
attribute, and two elements with the .screen-only
and sr-only
classes to update. These can be paragraphs or other elements but should also contain a text version of the number of allowed characters should the user have JavaScript disabled.
The rest of the HTML is a label for our input and a few supporting ARIA attributes for screen readers.
<label for="input_name">Please enter text (Required)</label>
<textarea id="input_name" maxlength="1000" class="counted" aria-describedby="counter_description"></textarea>
<p class="counter screen-only" aria-hidden="true">Enter up to 1000 characters</p>
<p class="counter sr-only" id="counter_description">Enter up to 1000 characters</p>
CSS
The only required CSS that is a generic class to visually hide your second counter. This is my preferred method but feel free to use whichever you're most comfortable with.
.sr-only {
position: absolute;
left: -9999em;
opacity: 0;
overflow: hidden;
top: -9999em;
}
JavaScript
Look, I'll be blunt. I'm terrible at JavaScript. This is the best I could cobble together. While functional, you're more than welcome to modify or improve on this.
document.addEventListener("DOMContentLoaded", function () {
var timer = '';
var counted = document.querySelectorAll('.counted');
counted.forEach(function (elem) {
elem.addEventListener("input", function () {
clearTimeout(timer);
var pause = this;
var limit = this.getAttribute("maxlength");
var remainingChars = limit - this.value.length;
if (remainingChars <= 0) {
this.value = this.value.substring(0, limit);
}
var screenOnlyElem = this.nextElementSibling;
while (screenOnlyElem && screenOnlyElem.classList.contains('screen-only') === false) {
screenOnlyElem = screenOnlyElem.nextElementSibling;
}
if (screenOnlyElem) {
screenOnlyElem.textContent = remainingChars <= -1 ? 0 : remainingChars + ' character(s) remaining';
}
timer = setTimeout(function () {
var srOnlyElem = pause.nextElementSibling;
while (srOnlyElem && srOnlyElem.classList.contains('sr-only') === false) {
srOnlyElem = srOnlyElem.nextElementSibling;
}
if (srOnlyElem) {
srOnlyElem.textContent = remainingChars <= -1 ? 0 : remainingChars + ' characters remaining';
}
}, 2000);
});
elem.dispatchEvent(new Event('input'));
});
});
jQuery
You'll need the jQuery library if you want to use this version.
$(document).ready(function () {
var timer = '';
$('.counted').on("load propertychange keyup input paste",
function () {
clearTimeout(timer);
var pause = $(this);
var limit = $(this).attr("maxlength");
var remainingChars = limit - $(this).val().length;
if (remainingChars <= 0) {
$(this).val($(this).val().substring(0, limit));
}
$(this).nextAll('.screen-only').first().text(remainingChars<=-1?0:remainingChars+' character(s) remaining');
timer = setTimeout(function() {
(pause).nextAll('.sr-only').first().text(remainingChars<=-1?0:remainingChars+' characters remaining');
}, 2000);
});
$('.counted').trigger('load');
});
Accessibility
There are a few accessibility-centric elements in this script. The most obvious is the second counter, which only updates following a period of inactivity. This is to prevent flooding screen readers with updates as they type. The visual counter is hidden from screen readers with the aria-hidden="true"
attribute.
Should the user not have JavaScript enabled, they will be provided with the static description of "Enter up to 1000 characters", which is hard-coded in the HTML and gets replaced by the script on load.
In my testing, it was not necessary to provide an aria-live
attribute to the counter.
As always, if you opt to use this solution, always validate that its working correctly using a few screen readers.
Customization
Inactivity time
The time it takes for the second counter to update can be changed with the value found in the timer function. The following example changes the value to five seconds, up from the default value of two.
JavaScript
if (srOnlyElem) {
srOnlyElem.textContent = remainingChars <= -1 ? 0 : remainingChars + ' characters remaining';
}
}, 5000);
jQuery
timer = setTimeout(function() {
(pause).nextAll('.sr-only').first().text(remainingChars<=-1?0:remainingChars+' characters remaining');
}, 5000);
Character limits
The maximum character length is based on the maxlength value of your element. The following example has a character limit decreased to 500.
<textarea id="input_description" maxlength="500" class="counted" aria-describedby="counter_description"></textarea>
<p class="counter screen-only" aria-hidden="true">Enter up to 500 characters</p>
<p class="counter sr-only" aria-live="polite" id="counter_description">Enter up to 500 characters</p>
Multiple inputs
The script supports multiple elements with an update to the label and counter IDs.
<label for="input_name1">Please enter text (Required)</label>
<textarea id="input_name1" maxlength="1000" class="counted" aria-describedby="counter_description1"></textarea>
<p class="counter screen-only" aria-hidden="true">Enter up to 1000 characters</p>
<p class="counter sr-only" id="counter_description1">Enter up to 1000 characters</p>
<label for="input_name2">Please enter text (Required)</label>
<textarea id="input_name2" maxlength="1000" class="counted" aria-describedby="counter_description2"></textarea>
<p class="counter screen-only" aria-hidden="true">Enter up to 1000 characters</p>
<p class="counter sr-only" id="counter_description2">Enter up to 1000 characters</p>