Skip to the code
cf.

Character counter for a textarea

EasyDOM

The page has a textarea with a maxlength and an empty paragraph for its count:

<label for="bio">Bio</label>
<textarea id="bio" maxlength="20">Hi</textarea>
<p id="bio-count" aria-live="polite"></p>

Write attachCounter(textarea, output). It sets output's text to how many more characters fit: the textarea's maxlength minus the length of its value, written as 18 characters left, or 1 character left when exactly one is left.

Show the count as soon as it's called, then update it on every input event: it fires for typing, pasting, cutting, and dictation alike. When 5 or fewer characters are left, give output the class warning, and take it off when more are left again.

Also set the textarea's aria-describedby to the output's id, so screen readers read the count with the field. Return a function that stops the updates.

const stop = attachCounter(document.getElementById("bio"), document.getElementById("bio-count"));
// the count reads "18 characters left" (the textarea starts with "Hi")
// the user changes it to "Hello": "15 characters left"
stop(); // no more updates

What the tests check

  1. shows the count as soon as it's attached
    const output = document.getElementById("bio-count");
    attachCounter(document.getElementById("bio"), output);
    expect(output.textContent).toBe("18 characters left");
    
  2. updates on every input event
    const textarea = document.getElementById("bio");
    const output = document.getElementById("bio-count");
    attachCounter(textarea, output);
    textarea.value = "Hello";
    fire(textarea, "input");
    expect(output.textContent).toBe("15 characters left");
    textarea.value = "";
    fire(textarea, "input");
    expect(output.textContent).toBe("20 characters left");
    
  3. says 1 character, not 1 characters
    const textarea = document.getElementById("bio");
    const output = document.getElementById("bio-count");
    attachCounter(textarea, output);
    textarea.value = "a".repeat(19);
    fire(textarea, "input");
    expect(output.textContent).toBe("1 character left");
    textarea.value = "a".repeat(20);
    fire(textarea, "input");
    expect(output.textContent).toBe("0 characters left");
    
  4. adds the warning class at 5 or fewer, and takes it off again
    const textarea = document.getElementById("bio");
    const output = document.getElementById("bio-count");
    attachCounter(textarea, output);
    textarea.value = "a".repeat(14);
    fire(textarea, "input");
    expect(output.classList.contains("warning")).toBe(false);
    textarea.value = "a".repeat(15);
    fire(textarea, "input");
    expect(output.classList.contains("warning")).toBe(true);
    textarea.value = "a".repeat(10);
    fire(textarea, "input");
    expect(output.classList.contains("warning")).toBe(false);
    
  5. reads the limit from the maxlength attribute
    const textarea = document.getElementById("bio");
    const output = document.getElementById("bio-count");
    textarea.setAttribute("maxlength", "5");
    attachCounter(textarea, output);
    expect(output.textContent).toBe("3 characters left");
    
  6. links the count to the textarea with aria-describedby
    const textarea = document.getElementById("bio");
    attachCounter(textarea, document.getElementById("bio-count"));
    expect(textarea.getAttribute("aria-describedby")).toBe("bio-count");
    
  7. the returned function stops the updates
    const textarea = document.getElementById("bio");
    const output = document.getElementById("bio-count");
    const stop = attachCounter(textarea, output);
    stop();
    textarea.value = "Hello";
    fire(textarea, "input");
    expect(output.textContent).toBe("18 characters left");
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).