javascript 27 lines · 7 steps

A live character counter for textareas

Attach an accessible, self-updating character count to any textarea and hand back a cleanup function.

Explained by highlit
1function initCharacterCounter(textarea, options = {}) {
2 const maxLength = options.maxLength ?? 280;
3 const warnThreshold = options.warnThreshold ?? 0.9;
4 
5 const counter = document.createElement('div');
6 counter.className = 'char-counter';
7 counter.setAttribute('aria-live', 'polite');
8 textarea.insertAdjacentElement('afterend', counter);
9 
10 const update = () => {
11 const length = [...textarea.value].length;
12 const remaining = maxLength - length;
13 
14 counter.textContent = `${remaining} character${remaining === 1 ? '' : 's'} remaining`;
15 counter.classList.toggle('char-counter--warning', length >= maxLength * warnThreshold);
16 counter.classList.toggle('char-counter--error', remaining < 0);
17 textarea.setAttribute('aria-invalid', String(remaining < 0));
18 };
19 
20 textarea.addEventListener('input', update);
21 update();
22 
23 return () => {
24 textarea.removeEventListener('input', update);
25 counter.remove();
26 };
27}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Returning a teardown function lets callers undo every side effect the setup created.
  2. 2aria-live and aria-invalid keep dynamic UI changes legible to screen readers.
  3. 3Spreading a string into an array counts Unicode code points rather than UTF-16 units.

Related explainers

javascript
const ROLE_PERMISSIONS = {
  admin: ['users:read', 'users:write', 'billing:read', 'billing:write'],
  manager: ['users:read', 'billing:read'],
  member: ['users:read'],

Role-based permissions middleware in Express

authorization middleware rbac
Intermediate 9 steps
javascript
function attachThousandSeparators(input, { locale = 'en-US' } = {}) {
  const formatter = new Intl.NumberFormat(locale);
  const groupSep = formatter.format(11111).replace(/\d/g, '')[0] || ',';
  const decimalSep = formatter.format(1.1).replace(/\d/g, '')[0] || '.';

Live thousand separators without losing the caret

dom intl caret-preservation
Advanced 8 steps
javascript
import { useReducer, useEffect } from "react";
 
const initialState = { status: "idle", data: null, error: null };
 

Building a data-fetching hook in React

custom-hooks usereducer data-fetching
Intermediate 9 steps
javascript
const express = require('express');
const app = express();
 
app.get('/health', (req, res) => res.json({ status: 'ok' }));

Graceful shutdown in an Express server

graceful-shutdown signal-handling connection-tracking
Advanced 9 steps
javascript
import { useState, useEffect, useCallback } from 'react';
 
function getColumnCount(width) {
  if (width < 640) return 1;

A responsive column hook in React

custom-hooks debouncing responsive-design
Intermediate 7 steps
javascript
function autoResizeTextarea(textarea, { maxHeight = Infinity } = {}) {
  const resize = () => {
    textarea.style.height = 'auto';
    const contentHeight = textarea.scrollHeight;

Auto-resizing a textarea to fit its content

dom event-listener cleanup
Intermediate 7 steps

Share this explainer

Here's the card — post it anywhere.

A live character counter for textareas — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code