Calculators & Utilities

Free Word Counter Widget

Live word, character, sentence, and reading-time counts as visitors type.

A simple word counter your visitors can use to check word count, characters (with and without spaces), sentence count, and estimated reading time as they type. It is great for blogs, writing tools, student resources, and editorial sites. Install it with a single script tag — no dependencies, no accounts.

Live demo

Embed this widget

Paste either snippet into your page — a Custom HTML block on WordPress, Shopify, or Squarespace works too. Both snippets update live as you customize, so what you copy is exactly what you configured.

Hosted (recommended)html
<script src="https://hayneslab.dev/widgets/word-counter.js" data-hl-placeholder="Paste your blog draft here to check the word count…" async></script>
<!-- Free Word Counter widget by haynes lab — https://hayneslab.dev/tools/widgets/word-counter -->
Inline (self-contained)html
<!-- Free Word Counter widget by haynes lab — https://hayneslab.dev/tools/widgets/word-counter -->
<script data-hl-placeholder="Paste your blog draft here to check the word count…">
/*!
 * haynes lab — Word Counter widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/word-counter
 */
(function () {
  'use strict';

  var PREFIX = 'hlw-word-counter';
  var STYLE_ID = 'hlw-word-counter-styles';

  function findScript() {
    return document.currentScript ||
      document.querySelector('script[src*="widgets/word-counter.js"]');
  }

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    var css =
      '.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
      '.' + PREFIX + ' { position: relative; max-width: 640px; margin: 0 auto; padding: 24px 24px 32px;' +
      ' font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;' +
      ' font-size: 16px; line-height: 1.5; color: #1f2937; background: #ffffff;' +
      ' border: 1px solid #e5e7eb; border-radius: 12px; }' +
      '.' + PREFIX + '__textarea { display: block; width: 100%; min-height: 140px; padding: 12px 14px;' +
      ' font: inherit; color: inherit; background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px;' +
      ' resize: vertical; }' +
      '.' + PREFIX + '__textarea::placeholder { color: #9ca3af; }' +
      '.' + PREFIX + '__textarea:focus { outline: 2px solid var(--hlw-accent); outline-offset: 1px;' +
      ' border-color: var(--hlw-accent); background: #ffffff; }' +
      '.' + PREFIX + '__stats { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; margin-top: 16px;' +
      ' list-style: none; padding: 0; }' +
      '.' + PREFIX + '__stat { padding: 10px 6px; text-align: center; background: #f9fafb;' +
      ' border: 1px solid #f3f4f6; border-radius: 8px; }' +
      '.' + PREFIX + '__value { display: block; font-size: 20px; font-weight: 700; line-height: 1.2;' +
      ' color: var(--hlw-accent); font-variant-numeric: tabular-nums; }' +
      '.' + PREFIX + '__label { display: block; margin-top: 2px; font-size: 11px; letter-spacing: 0.02em;' +
      ' text-transform: uppercase; color: #6b7280; }' +
      '.' + PREFIX + '__clear { margin-top: 14px; padding: 7px 14px; font: inherit; font-size: 13px;' +
      ' color: #6b7280; background: #ffffff; border: 1px solid #e5e7eb; border-radius: 8px; cursor: pointer; }' +
      '.' + PREFIX + '__clear:hover { color: var(--hlw-accent); border-color: var(--hlw-accent); }' +
      '.' + PREFIX + '__clear:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 2px; }' +
      '.' + PREFIX + '__credit { position: absolute; right: 10px; bottom: 6px; font-size: 11px;' +
      ' display: inline-flex; align-items: center; gap: 4px; color: #9ca3af; text-decoration: none; }' +
      '.' + PREFIX + '__credit svg { display: block; }' +
      '.' + PREFIX + '__credit:hover { color: #6b7280; text-decoration: underline; }' +
      '@media (max-width: 560px) { .' + PREFIX + ' { padding: 18px 16px 30px; }' +
      ' .' + PREFIX + '__stats { grid-template-columns: repeat(3, 1fr); } }';
    var style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = css;
    document.head.appendChild(style);
  }

  function el(tag, className, text) {
    var node = document.createElement(tag);
    if (className) node.className = className;
    if (text != null) node.textContent = text;
    return node;
  }

  function logoSvg() {
    var svgNS = 'http://www.w3.org/2000/svg';
    var svg = document.createElementNS(svgNS, 'svg');
    svg.setAttribute('viewBox', '0 0 32 32');
    svg.setAttribute('width', '13');
    svg.setAttribute('height', '13');
    svg.setAttribute('aria-hidden', 'true');
    svg.setAttribute('focusable', 'false');
    function circle(cx, cy, r, attrs) {
      var c = document.createElementNS(svgNS, 'circle');
      c.setAttribute('cx', cx);
      c.setAttribute('cy', cy);
      c.setAttribute('r', r);
      for (var key in attrs) c.setAttribute(key, attrs[key]);
      svg.appendChild(c);
    }
    circle(16, 16, 16, { fill: '#0f0a0a' });
    circle(16, 16, 12, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.3 });
    circle(16, 16, 9, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.5 });
    circle(16, 16, 6, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1.2, opacity: 0.9 });
    circle(16, 16, 3, { fill: '#06b6d4' });
    circle(24.5, 7.5, 2, { fill: '#f97316' });
    circle(7, 16, 1.8, { fill: '#f97316' });
    circle(20, 20, 1.5, { fill: '#f97316' });
    return svg;
  }

  function countWords(text) {
    var trimmed = text.trim();
    if (!trimmed) return 0;
    return trimmed.split(/\s+/).length;
  }

  function countSentences(text) {
    var matches = text.match(/[^.!?]+[.!?]+/g);
    if (matches) return matches.length;
    return text.trim() ? 1 : 0;
  }

  function readingTime(words) {
    if (!words) return '0 min';
    var minutes = words / 200;
    if (minutes < 1) return Math.max(1, Math.round(minutes * 60)) + ' sec';
    var rounded = Math.round(minutes * 10) / 10;
    return rounded + ' min';
  }

  function build(script) {
    var placeholder = script.getAttribute('data-hl-placeholder') ||
      'Start typing or paste your text here…';
    var accent = script.getAttribute('data-hl-accent') || '#f97316';

    injectStyles();

    var root = el('div', PREFIX);
    root.setAttribute('role', 'region');
    root.setAttribute('aria-label', 'Word counter');
    root.style.setProperty('--hlw-accent', accent);

    var textarea = el('textarea', PREFIX + '__textarea');
    textarea.setAttribute('placeholder', placeholder);
    textarea.setAttribute('aria-label', 'Text to count');
    textarea.setAttribute('rows', '6');
    root.appendChild(textarea);

    var stats = el('ul', PREFIX + '__stats');
    var defs = [
      { label: 'Words', value: function (t) { return String(countWords(t)); } },
      { label: 'Characters', value: function (t) { return String(t.length); } },
      { label: 'No spaces', value: function (t) { return String(t.replace(/\s/g, '').length); } },
      { label: 'Sentences', value: function (t) { return String(countSentences(t)); } },
      { label: 'Reading time', value: function (t) { return readingTime(countWords(t)); } },
    ];
    var valueNodes = defs.map(function (d) {
      var item = el('li', PREFIX + '__stat');
      var value = el('span', PREFIX + '__value', '0');
      item.appendChild(value);
      item.appendChild(el('span', PREFIX + '__label', d.label));
      stats.appendChild(item);
      return value;
    });
    root.appendChild(stats);

    function update() {
      var text = textarea.value;
      defs.forEach(function (d, i) {
        valueNodes[i].textContent = d.value(text);
      });
    }
    textarea.addEventListener('input', update);

    var clear = el('button', PREFIX + '__clear', 'Clear text');
    clear.type = 'button';
    clear.addEventListener('click', function () {
      textarea.value = '';
      update();
      textarea.focus();
    });
    root.appendChild(clear);

    var credit = el('a', PREFIX + '__credit');
    credit.appendChild(logoSvg());
    credit.appendChild(document.createTextNode('Powered by haynes lab'));
    credit.href = 'https://hayneslab.dev/tools/widgets/word-counter';
    credit.target = '_blank';
    credit.rel = 'noopener';
    root.appendChild(credit);

    update();

    if (script.parentNode && script.parentNode.nodeName !== 'HEAD') {
      script.insertAdjacentElement('afterend', root);
    } else {
      document.body.appendChild(root);
    }
  }

  function init() {
    var script = findScript();
    if (!script) {
      console.warn('[haynes lab word-counter] Could not locate its own script tag. Nothing rendered.');
      return;
    }
    build(script);
  }

  if (document.body) {
    init();
  } else {
    document.addEventListener('DOMContentLoaded', init);
  }
})();

</script>

Configuration

Every option is an attribute on the script tag — the same attributes the customizer above exposes. All attributes are optional unless marked required — defaults apply when omitted.

AttributeDefaultDescription
data-hl-placeholder"Start typing or paste your text here…"Placeholder shown inside the empty textarea.
data-hl-accent"#f97316"Highlight color for the stat numbers and focus states.

Frequently asked questions

How do I install the Word Counter?

Paste the script snippet where you want the counter to appear — it renders inline right after the tag and starts counting the moment someone types or pastes text.

Can I change the placeholder text and colors?

Yes — set data-hl-placeholder for the textarea placeholder text and data-hl-accent for the highlight color used on stat numbers and focus outlines.

Does it work on WordPress, Shopify, or Squarespace?

Yes — it is a single vanilla JS file with no dependencies, so it works on WordPress, Shopify, Squarespace, or any site that can embed a script tag.

More free widgets

Need a custom widget?

We build custom widgets, integrations, and whole products. Tell us what your site needs and we'll scope it.

Get in Touch