Build a real-time character counter for WordPress forms with JavaScript

Character limits are a common UX requirement for forms: Twitter-style inputs, meta description fields, SMS message composers, and custom comment fields all benefit from a real-time counter that tells the user exactly how many characters they have used and how many remain. Implementing this in vanilla JavaScript takes fewer than thirty lines of code and works with any WordPress form, whether rendered by a page builder, Contact Form 7, Gravity Forms, or a completely custom theme template. The key event to listen to is input rather than keyupinput fires on paste, drag-and-drop, autocomplete, and voice input as well as keyboard events, giving accurate counts in every interaction mode. You can combine this pattern with the debounce guide to delay server-side validation while keeping the counter instant. The counter should be visually clear: show the remaining characters, change colour as the limit approaches, and optionally disable the submit button when the limit is exceeded. Accessibility matters too — wrap the counter in an aria-live="polite" region so screen readers announce updates without interrupting the user. This article also shows how to apply the counter to dynamically added fields (using event delegation on a parent container) and how to initialise it for fields that already have a value when the page loads. The pattern requires zero jQuery and works in all modern browsers. Combine it with the event delegation guide to handle fields added after page load.

Problem: You need to show users a real-time character count on a WordPress form textarea or input so they know how many characters they have used before hitting the server-side limit.

Solution: Add a data-max-length attribute to any field and attach the following JavaScript counter that fires on every input event:

/**
 * Initialise a character counter for a single field.
 * @param {HTMLInputElement|HTMLTextAreaElement} field
 * @param {number} maxLength
 */
function initCharCounter( field, maxLength ) {
    // Create counter element
    const counter = document.createElement( 'span' );
    counter.setAttribute( 'aria-live', 'polite' );
    counter.classList.add( 'char-counter' );
    field.insertAdjacentElement( 'afterend', counter );

    function update() {
        const used      = field.value.length;
        const remaining = maxLength - used;

        counter.textContent = `${used} / ${maxLength}`;
        counter.classList.toggle( 'char-counter--warn',  remaining <= 20 && remaining > 0 );
        counter.classList.toggle( 'char-counter--error', remaining < 0 );

        // Disable nearest submit button when over the limit
        const form   = field.closest( 'form' );
        const submit = form && form.querySelector( '[type="submit"]' );
        if ( submit ) {
            submit.disabled = remaining < 0;
        }
    }

    field.addEventListener( 'input', update );
    update(); // initialise for pre-filled values
}

// Apply to every field with data-max-length attribute
document.querySelectorAll( '[data-max-length]' ).forEach( function ( field ) {
    initCharCounter( field, parseInt( field.dataset.maxLength, 10 ) );
} );

// Event delegation for dynamically added fields
document.addEventListener( 'input', function ( e ) {
    const field = e.target.closest( '[data-max-length]' );
    if ( ! field || field._counterInit ) return;
    field._counterInit = true;
    initCharCounter( field, parseInt( field.dataset.maxLength, 10 ) );
} );

<!-- Usage: add data-max-length to any input or textarea -->
<textarea name="message" data-max-length="280" rows="4"></textarea>
<input type="text" name="title" data-max-length="60" />

NOTE: The data-max-length attribute is a hint for the JavaScript counter and does not replace the maxlength HTML attribute or server-side validation. Always validate length on the server before saving — see the input sanitisation guide for the WordPress sanitisation functions to use. The CSS classes char-counter--warn and char-counter--error can be styled freely in your theme stylesheet.