Marketing & Social

Free Reading Time Widget

Show an "X min read" badge on any article, calculated automatically.

A small inline badge that counts the words in your article and displays an estimated reading time, like "6 min read". It is built for blogs, news sites, and documentation pages that want to set reader expectations up front. Install is one script tag — point it at your content selector and it does the math on every page load.

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/reading-time.js" async></script>
<!-- Free Reading Time widget by haynes lab — https://hayneslab.dev/tools/widgets/reading-time -->
Inline (self-contained)html
<!-- Free Reading Time widget by haynes lab — https://hayneslab.dev/tools/widgets/reading-time -->
<script>
/*!
 * haynes lab — Reading Time widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/reading-time
 */
(function () {
  'use strict';

  var PREFIX = 'hlw-reading-time';
  var STYLE_ID = 'hlw-reading-time-styles';

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

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    var css =
      '.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
      '.' + PREFIX + ' { display: inline-flex; align-items: baseline; gap: 8px; flex-wrap: wrap;' +
      ' font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;' +
      ' font-size: 13px; line-height: 1.5; color: #6b7280; }' +
      '.' + PREFIX + '__badge { display: inline-flex; align-items: center; gap: 6px;' +
      ' padding: 4px 12px; background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 999px;' +
      ' color: #374151; font-size: 13px; font-weight: 500; white-space: nowrap; }' +
      '.' + PREFIX + '__icon { display: block; flex: 0 0 auto; }' +
      '.' + PREFIX + '__credit { 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; }';
    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 clockSvg(accent) {
    var svgNS = 'http://www.w3.org/2000/svg';
    var svg = document.createElementNS(svgNS, 'svg');
    svg.setAttribute('viewBox', '0 0 16 16');
    svg.setAttribute('width', '14');
    svg.setAttribute('height', '14');
    svg.setAttribute('aria-hidden', 'true');
    svg.setAttribute('focusable', 'false');
    svg.setAttribute('class', PREFIX + '__icon');
    var face = document.createElementNS(svgNS, 'circle');
    face.setAttribute('cx', '8');
    face.setAttribute('cy', '8');
    face.setAttribute('r', '6.5');
    face.setAttribute('fill', 'none');
    face.setAttribute('stroke', accent);
    face.setAttribute('stroke-width', '1.5');
    svg.appendChild(face);
    var hands = document.createElementNS(svgNS, 'path');
    hands.setAttribute('d', 'M8 4.5 V8 L10.8 9.8');
    hands.setAttribute('fill', 'none');
    hands.setAttribute('stroke', accent);
    hands.setAttribute('stroke-width', '1.5');
    hands.setAttribute('stroke-linecap', 'round');
    svg.appendChild(hands);
    return svg;
  }

  function countWords(node) {
    var clone = node.cloneNode(true);
    var skip = clone.querySelectorAll('script, style, noscript, .' + PREFIX);
    Array.prototype.forEach.call(skip, function (n) {
      if (n.parentNode) n.parentNode.removeChild(n);
    });
    var text = (clone.textContent || '').replace(/\s+/g, ' ').trim();
    if (!text) return 0;
    return text.split(' ').length;
  }

  function findTarget(script) {
    var selector = script.getAttribute('data-hl-target');
    if (selector) {
      try {
        var chosen = document.querySelector(selector);
        if (chosen) return chosen;
        console.warn('[haynes lab reading-time] data-hl-target "' + selector + '" matched nothing. Falling back to article/body.');
      } catch (e) {
        console.warn('[haynes lab reading-time] data-hl-target "' + selector + '" is not a valid selector. Falling back to article/body.', e);
      }
    }
    return document.querySelector('article') || document.body;
  }

  function build(script) {
    var wpm = parseInt(script.getAttribute('data-hl-wpm'), 10);
    if (isNaN(wpm) || wpm < 50 || wpm > 1000) wpm = 200;
    var accent = script.getAttribute('data-hl-accent') || '#f97316';

    var target = findTarget(script);
    var words = countWords(target);
    var minutes = Math.max(1, Math.ceil(words / wpm));

    injectStyles();

    var root = el('span', PREFIX);
    var label = minutes + ' min read';
    var badge = el('span', PREFIX + '__badge');
    badge.setAttribute('aria-label', 'Estimated reading time: ' + label);
    badge.appendChild(clockSvg(accent));
    badge.appendChild(document.createTextNode(label));
    root.appendChild(badge);

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

    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 reading-time] 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-target(auto)CSS selector for the content to measure. When omitted, the widget uses the first <article> element, or the whole page body if there is none.
data-hl-wpm"200"Reading speed used for the estimate. 200 wpm is the common average for adult readers; accepted range is 50–1000.
data-hl-accent"#f97316"Any CSS color used for the clock icon inside the badge.

Frequently asked questions

How do I install the reading time badge?

Paste the embed snippet where you want the badge to appear — typically right under your article title or byline. It counts the words in your content and renders an inline "X min read" pill at that spot.

How do I change what gets counted or the reading speed?

Set data-hl-target to any CSS selector (e.g. ".post-content" or "#main") to measure a specific container — by default it uses the first <article> element, or the page body. Set data-hl-wpm to adjust the words-per-minute speed, and data-hl-accent to recolor the clock icon.

Does it work on WordPress, Shopify, or Squarespace?

Yes. It is a single dependency-free vanilla JS file with scoped styles. Drop the script tag into a Custom HTML block on any platform, or straight into your template, and it computes the estimate on page load.

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