TEXT

Reading Time Estimator

Paste any text to instantly estimate reading time, speaking time, complexity, and more.

Reading speed
Slow 150 wpm
Average 200 wpm
Fast 300 wpm
—
Reading Time
—
Speaking Time
0
Words
0
Characters
0
Sentences
0
Unique Words
0
Avg Word Length
—
Longest Word
Complexity Analysis
Avg sentence length: — words
Reading level:

About this tool

This free reading time estimator updates live as you type. It calculates how long your text takes to read or speak aloud, and gives you a complexity breakdown based on sentence length.

Reading speeds: Slow readers average ~150 wpm, most adults read at ~200 wpm, and fast readers reach ~300 wpm. Speaking time is fixed at 130 wpm, suitable for presentations and narration.

Reading level is based on average sentence length: under 10 words per sentence is Easy, 10–20 is Medium, and over 20 is Complex.

Developer Reference

Core Algorithm & Standalone Script

Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.

const textarea  = document.getElementById('text-input');
  const clearBtn  = document.getElementById('clear-btn');
  const wpmToggle = document.getElementById('wpm-toggle');

  let selectedWpm = 200;

  // WPM toggle
  wpmToggle.addEventListener('click', e => {
    const item = e.target.closest('.toggle-item');
    if (!item) return;
    wpmToggle.querySelectorAll('.toggle-item').forEach(i => i.classList.remove('active'));
    item.classList.add('active');
    selectedWpm = parseInt(item.dataset.wpm, 10);
    update();
  });

  function formatTime(words, wpm) {
    if (words === 0) return '—';
    const mins = words / wpm;
    if (mins < 1) return '< 1 min';
    const h = Math.floor(mins / 60);
    const m = Math.floor(mins % 60);
    if (h > 0) return m > 0 ? `${h} hr ${m} min` : `${h} hr`;
    return `${m} min`;
  }

  function flash(cardId) {
    const el = document.getElementById(cardId);
    if (!el) return;
    el.classList.add('flash');
    setTimeout(() => el.classList.remove('flash'), 150);
  }

  function setVal(id, cardId, val) {
    const el = document.getElementById(id);
    const newVal = String(val);
    if (el && el.textContent !== newVal) {
      if (cardId) flash(cardId);
      el.textContent = newVal;
    }
  }

  function update() {
    const text = textarea.value;
    const trimmed = text.trim();

    // Word list
    const wordList = trimmed === '' ? [] : trimmed.split(/\s+/).filter(w => w.length > 0);
    const words = wordList.length;

    // Chars
    const chars = text.length;

    // Sentences
    const sentenceMatches = trimmed === '' ? [] : (trimmed.match(/[^.!?]*[.!?]+/g) || []);
    const sentences = sentenceMatches.length;

    // Unique words (case-insensitive, strip punctuation)
    const cleanWords = wordList.map(w => w.toLowerCase().replace(/[^a-z0-9']/g, ''));
    const unique = new Set(cleanWords.filter(w => w.length > 0)).size;

    // Avg word length
    const totalChars = cleanWords.reduce((s, w) => s + w.length, 0);
    const avgWordLen = words > 0 ? (totalChars / words).toFixed(1) : '0';

    // Longest word
    const longestWord = words > 0
      ? wordList.reduce((a, b) => (a.replace(/[^a-zA-Z]/g,'').length >= b.replace(/[^a-zA-Z]/g,'').length ? a : b), '')
      : '—';

    // Avg sentence length
    const avgSentLen = sentences > 0 ? (words / sentences).toFixed(1) : '—';

    // Reading level
    let level = 'none';
    if (sentences > 0) {
      const avg = words / sentences;
      if (avg < 10) level = 'easy';
      else if (avg <= 20) level = 'medium';
      else level = 'complex';
    }

    // Times
    const readTime  = formatTime(words, selectedWpm);
    const speakTime = formatTime(words, 130);

    // Update DOM
    setVal('val-readtime',  'card-readtime',  readTime);
    setVal('val-speaktime', 'card-speaktime', speakTime);
    setVal('val-words',     'card-words',     words);
    setVal('val-chars',     'card-chars',     chars);
    setVal('val-sentences', 'card-sentences', sentences);
    setVal('val-unique',    'card-unique',    unique);
    setVal('val-avgword',   'card-avgword',   avgWordLen);

    // Longest word
    const longestEl = document.getElementById('val-longest');
    if (longestEl) {
      const display = longestWord.length > 16 ? longestWord.slice(0, 14) + '…' : longestWord;
      if (longestEl.textContent !== display) longestEl.textContent = display;
      longestEl.title = longestWord !== '—' ? longestWord : '';
    }

    // Complexity
    setVal('val-avgsentlen', null, avgSentLen);
    document.getElementById('badge-easy').classList.add('hidden');
    document.getElementById('badge-medium').classList.add('hidden');
    document.getElementById('badge-complex').classList.add('hidden');
    if (level !== 'none') document.getElementById('badge-' + level).classList.remove('hidden');

    // Chapter estimates
    const chapterCard = document.getElementById('chapter-card');
    if (words > 100) {
      chapterCard.classList.remove('hidden');
      const chapterWords = 2000;
      const chapters = Math.ceil(words / chapterWords);
      const chapterReadTime = formatTime(chapterWords, selectedWpm);
      const lastChapterWords = words % chapterWords || chapterWords;
      const lastChapterTime  = formatTime(lastChapterWords, selectedWpm);

      setVal('val-chapters', null, chapters);
      setVal('val-chapter-readtime', null, chapterReadTime);

      const detail = document.getElementById('chapter-detail');
      if (chapters > 1) {
        detail.textContent = `${chapters - 1} full chapter${chapters - 1 > 1 ? 's' : ''} of ~${chapterWords.toLocaleString()} words + 1 partial chapter of ~${lastChapterWords.toLocaleString()} words (${lastChapterTime} to read).`;
      } else {
        detail.textContent = `Your text fits in a single chapter of ~${words.toLocaleString()} words.`;
      }
    } else {
      chapterCard.classList.add('hidden');
    }

    // Auto-resize textarea
    textarea.style.height = 'auto';
    textarea.style.height = Math.max(220, textarea.scrollHeight) + 'px';
  }

  textarea.addEventListener('input', update);

  clearBtn.addEventListener('click', () => {
    textarea.value = '';
    textarea.style.height = '220px';
    update();
    textarea.focus();
  });

  update();