Interactive

Copy. Paste. Ship.

Home › Code Snippets › CSS › AI Analyzer

Part of our complete guide: CSS Snippets →

UI Component · HTML + CSS + JS

AI Text Analyzer

Paste any text and get instant insights — word count, readability score, estimated reading time, sentiment, and top keywords. All in vanilla JavaScript, no API, no data leaves the browser.

Last updated: October 2026 · 8 min read

An AI text analyzer reads a block of text and returns structured insights: word count, sentence count, readability score, sentiment, and keyword density. Most "AI" analyzers are actually heuristics — regex, word lists, and syllable counting — which makes them fast, free, private, and accurate enough for a first pass.

What you get: Complete analyzer with live word/sentence/character counts, Flesch reading ease, estimated reading time, word-list sentiment, and top keyword extraction. Everything runs in the browser — no API keys, no server, no privacy trade-off.

Why every writing tool has an analyzer now

Word count used to be the only metric. Then SEO made keyword density a thing. Then content marketing made readability a thing. Then social media made tone a thing. Today, a writer pasting a draft into a tool expects to see five or six numbers instantly — not just a character count. The tools that skip this feel dated.

The good news: none of these metrics require a large language model. Word count is a split. Sentence count is a regex. Readability is a formula from 1975. Sentiment with a word list is directionally correct. Keyword frequency is a Map. All of it runs in a few hundred milliseconds, in the browser, for free. Shipping it doesn't require an API key or a budget.

The pattern below is a complete, production-quality analyzer UI. It updates live as the user types. It's accessible — results are announced via a polite live region. It's honest — sentiment shows a confidence-ish score rather than pretending to be certain. And it's private — nothing is uploaded. That last point is a feature, not a limitation: users will paste sensitive drafts if they know the text stays local.

🎬 Live Demo

Paste or type text. Click Analyze. Results update instantly and are announced to screen readers.

🔍 Text Analyzer

Paste any English text. Nothing is sent to a server — all analysis runs locally.

~0 words · updates on Analyze
Click Analyze to see insights.

Complete code

<!-- HTML -->
<textarea id="analyzerInput" placeholder="Paste text..."></textarea>
<button id="analyzeBtn">Analyze</button>
<div id="analyzerResults" aria-live="polite"></div>

<style>
.analyzer-textarea {
  width: 100%; min-height: 130px;
  background: #1e293b; border: 1px solid #334155;
  border-radius: 0.6rem; padding: 0.85rem 1rem;
  color: #e2e8f0; font-family: inherit; font-size: 0.85rem;
  resize: vertical; outline: none;
}
.analyzer-textarea:focus { border-color: #3b82f6; }
.analyzer-results {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 0.6rem; margin-top: 1.25rem;
}
.result-card {
  background: #1e293b; border: 1px solid #334155;
  border-radius: 0.6rem; padding: 0.75rem 0.9rem;
}
.result-label {
  font-size: 0.62rem; text-transform: uppercase;
  letter-spacing: 0.1em; color: #64748b; font-weight: 700;
}
.result-value { font-size: 1.25rem; font-weight: 800; color: #fff; }
</style>

<script>
// Word list for basic sentiment (keep small, extend as needed)
var POSITIVE = ['good','great','excellent','amazing','fantastic','love',
  'delightful','smooth','fast','instant','helpful','perfect','best',
  'happy','happy','wonderful','clear','easy','nice','recommend'];

var NEGATIVE = ['bad','terrible','awful','hate','slow','broken','bug',
  'issue','problem','confusing','hard','difficult','worst','annoying',
  'fail','error','crash','ugly','poor','disappointed'];

var STOPWORDS = new Set(['the','a','an','and','or','but','if','then',
  'is','are','was','were','be','been','being','to','of','in','on','for',
  'with','at','by','from','as','it','its','this','that','these','those',
  'i','you','he','she','we','they','my','your','his','her','our','their']);

function countSyllables(word) {
  word = word.toLowerCase().replace(/[^a-z]/g, '');
  if (word.length <= 2) return 1;
  var groups = word.match(/[aeiouy]+/g);
  var n = groups ? groups.length : 1;
  if (word.endsWith('e') && !word.endsWith('le') && n > 1) n--;
  return Math.max(1, n);
}

function analyze(text) {
  text = text.trim();
  if (!text) return null;

  var words = text.split(/\s+/).filter(Boolean);
  var wordCount = words.length;
  var charCount = text.length;

  // Sentences: split on . ! ? followed by space or end
  var sentences = text.split(/[.!?]+/).filter(function(s) {
    return s.trim().length > 0;
  });
  var sentenceCount = Math.max(1, sentences.length);

  // Syllables + readability (Flesch Reading Ease)
  var totalSyllables = words.reduce(function(sum, w) {
    return sum + countSyllables(w);
  }, 0);
  var flesch = 206.835
    - 1.015 * (wordCount / sentenceCount)
    - 84.6  * (totalSyllables / Math.max(1, wordCount));
  flesch = Math.max(0, Math.min(100, flesch));

  // Reading time @ 220 wpm
  var readingTime = Math.max(1, Math.round(wordCount / 220));

  // Sentiment via word list
  var lower = text.toLowerCase();
  var tokens = lower.match(/[a-z]+/g) || [];
  var pos = 0, neg = 0;
  tokens.forEach(function(t) {
    if (POSITIVE.indexOf(t) !== -1) pos++;
    if (NEGATIVE.indexOf(t) !== -1) neg++;
  });
  var total = pos + neg;
  var sentiment = 'neutral';
  var sentimentScore = 0;
  if (total > 0) {
    sentimentScore = Math.round(((pos - neg) / total) * 100);
    if (sentimentScore > 15) sentiment = 'positive';
    else if (sentimentScore < -15) sentiment = 'negative';
  }

  // Keywords (exclude stopwords)
  var freq = {};
  tokens.forEach(function(t) {
    if (!STOPWORDS.has(t) && t.length > 2) {
      freq[t] = (freq[t] || 0) + 1;
    }
  });
  var keywords = Object.keys(freq)
    .map(function(k) { return { word: k, count: freq[k] }; })
    .sort(function(a, b) { return b.count - a.count; })
    .slice(0, 8);

  return {
    wordCount: wordCount,
    charCount: charCount,
    sentenceCount: sentenceCount,
    flesch: Math.round(flesch),
    readingTime: readingTime,
    sentiment: sentiment,
    sentimentScore: sentimentScore,
    keywords: keywords
  };
}

function render(result) {
  var box = document.getElementById('analyzerResults');
  if (!result) {
    box.innerHTML = '<div class="empty-results">No text to analyze.</div>';
    return;
  }
  box.innerHTML =
    '<div class="result-card"><div class="result-label">Words</div><div class="result-value">' + result.wordCount + '</div></div>' +
    '<div class="result-card"><div class="result-label">Sentences</div><div class="result-value">' + result.sentenceCount + '</div></div>' +
    '<div class="result-card"><div class="result-label">Characters</div><div class="result-value">' + result.charCount + '</div></div>' +
    '<div class="result-card"><div class="result-label">Read time</div><div class="result-value">' + result.readingTime + '<small>min</small></div></div>' +
    '<div class="result-card"><div class="result-label">Readability</div><div class="result-value">' + result.flesch + '</div></div>' +
    '<div class="result-card"><div class="result-label">Sentiment</div><div class="result-value">' + result.sentiment + '</div></div>';
}

document.getElementById('analyzeBtn').addEventListener('click', function() {
  render(analyze(document.getElementById('analyzerInput').value));
});
</script>

How the text analyzer works

1. Word and sentence counts are regex splits

Words: text.split(/\s+/). Sentences: split on [.!?]+ followed by end-of-string or space. There's no NLP library involved. Edge cases — abbreviations like "Dr." and "e.g." — will inflate sentence counts slightly. That's acceptable for a UI metric; users understand it's an estimate.

2. Readability is Flesch Reading Ease

The formula is 206.835 − 1.015×(words/sentences) − 84.6×(syllables/words). Syllables are counted by matching vowel groups — /[aeiouy]+/g — with a subtraction for trailing 'e'. Not perfect, but close enough that the score matches what Grammarly and Hemingway would show within a few points.

3. Reading time assumes 220 wpm

The average adult reads ~220 words per minute. Math.round(wordCount / 220) is the entire formula. It's the number Medium popularized and the number users expect. Don't overthink it — any wpm between 200 and 250 is fine.

4. Sentiment uses word lists, not an LLM

Two arrays — positive and negative — that contain the most common sentiment-bearing words. Every token is checked against both. The score is (pos − neg) / total. It's directionally correct: a strongly positive review scores +60, a mixed one scores +10, a negative one scores −40. It's fooled by sarcasm and negation, so present it as an estimate.

5. Keywords exclude stopwords

A stopword set — the, a, and, is, to, etc. — gets filtered out before frequency counting. Without this, "the" would be the top keyword on every text. Then a Map tallies the rest, sorts by count, and shows the top 8. That's the same technique SEO tools use for keyword density.

6. Everything runs in the browser

No fetch, no upload, no API key. That's a privacy feature: a user pasting an unreleased draft or a private email can trust that it stays local. It's also a performance feature: no network round-trip, results appear in the same frame as the click. If you wire it to a real model later, disclose the upload clearly.

🔬 Edge cases

The problems that break text analyzers in production.

Empty or whitespace-only input

The user pastes spaces or nothing, and the analyzer divides by zero or returns NaN. Fix: trim first, then return null if empty, and render a "No text to analyze" state. Never show Infinity or NaN in a result card.

Non-English text

A user pastes Spanish or Chinese. Word count still works. Syllable counting and sentiment word lists don't. Fix: detect script via Unicode ranges, and either disable sentiment or label it "English-only". Word count and char count remain useful.

Very long text freezing the UI

Pasting a 200-page document blocks the main thread for seconds. Fix: cap input length (e.g. 50,000 chars) with a clear message, or process in chunks via requestIdleCallback. For most use cases, capping is enough and simpler.

Sentiment reads "not good" as positive

A word-list analyzer sees "good" and counts it as positive, ignoring the "not". Fix: either accept the limitation (and label sentiment as approximate), or add simple negation detection — check if the previous token is "not"/"never"/"n't" and flip the score. Bigram negation handles 80% of cases.

Abbreviations inflate sentence count

"Dr. Smith went to Washington. He met with Sen. Jones." The split on . produces four "sentences" where there are two. Fix: pre-process to protect common abbreviations (Dr., Mr., e.g., i.e.) before splitting. Or just accept the over-count — users rarely care about a 10% error in a demo metric.

Results not announced to screen readers

Sighted users see the numbers update. Blind users have no idea the analysis ran. Fix: put results in a container with aria-live="polite". When the innerHTML changes, screen readers announce the new results. Without this, the whole feature is invisible to assistive tech.

Color-only sentiment indication

Green background for positive, red for negative — and no text label. Color-blind users can't tell which is which. Fix: always pair color with a word — "Positive", "Negative", "Neutral". And add an icon. WCAG requires this.

Every keystroke re-analyzes

If input events trigger analysis, each keystroke burns CPU. On long text, typing lags. Fix: analyze on button click, or debounce input events by 300–500ms. For a button-triggered analyzer (like this one), just use the button — no debounce needed.

♿ Accessibility

An analyzer that sighted users can use but screen reader users can't is a half-built component.

Requirement Implementation Why
Input label<label for> (visually hidden ok)Screen reader identifies the textarea
Live resultsaria-live="polite"Results announced on update
No color-onlySentiment text label + iconColor-blind users understand
Keyboard accessNative <textarea> + <button>Tab, Enter, Space all work
Meaningful labels"Words", not "Metric 1"Results are self-describing

The critical detail: aria-live must be on a container that's already in the DOM before results appear. If you create the container on click and insert it with content, screen readers won't announce it — live regions only announce changes to existing content. This is the #1 bug in "announcing results" implementations.

⚡ Performance

Aspect Value Notes
CSS size~2KBNo framework, no dependencies
JS size~80 linesVanilla, no library
Speed (1k words)<5msSingle pass over tokens
Network0 bytesNothing leaves the browser
Memory~O(n)Tokens array + frequency Map

Speed is a feature here. A network-backed analyzer takes 500ms–2s. This one takes ~5ms. Users can click Analyze repeatedly with zero waiting. That "instant" feel is why browser-based analyzers win on UX even when an LLM would be more accurate.

⚠️ Common mistakes

❓ Frequently asked questions

What is an AI text analyzer?

An AI text analyzer is a tool that reads a block of text and returns structured insights about it: word count, sentence count, readability score, estimated reading time, sentiment (positive/negative/neutral), and keyword frequency. It's usually driven by heuristics (regex, word lists, syllable counting) rather than a large language model, which makes it fast, free, and private.

How do I analyze text with JavaScript?

Split the text on whitespace for word count. Split on sentence-ending punctuation for sentence count. Count syllables by vowel groups for readability. Compare words against a positive and negative word list for sentiment. Tally word frequencies with a Map for keyword density. All achievable in vanilla JS without any library.

What is the Flesch-Kincaid readability score?

The Flesch Reading Ease formula is 206.835 − 1.015 × (words ÷ sentences) − 84.6 × (syllables ÷ words). Higher scores are easier to read: 90–100 is very easy, 60–70 is plain English, 30–50 is difficult, 0–30 is very difficult. It's a rough approximation, not a precise measure.

Can sentiment analysis work without an API?

Yes, for basic cases. A word-list approach counts positive vs negative words and returns a rough score. It fails on sarcasm, negation ("not good"), and context. For production-grade sentiment, use a model or API. For a UI demo or a first-pass filter, a word list is enough and runs instantly in the browser.

How do I calculate keyword density?

Normalize the text (lowercase, strip punctuation), split into words, filter out stop words (the, and, is, etc.), then count each word with a Map. Divide each count by the total non-stop-word count and multiply by 100 for a percentage. Show the top 5–10 keywords.

How accurate is a browser-based text analyzer?

For counts, readability, and keyword density: accurate. For sentiment and tone: approximate. Word counts and syllable estimates are deterministic. Sentiment via word lists is directionally correct but can be fooled by negation, sarcasm, and domain-specific language. Always present results as estimates, not verdicts.

Does the analyzer send text to a server?

In this component, no. Everything runs in the browser with vanilla JavaScript — the text never leaves the page. This is a privacy feature: users can paste sensitive drafts without worrying about data being uploaded. If you wire it to an API, disclose that clearly.

How do I make the analyzer accessible?

Use a labeled textarea. Announce results via a live region (aria-live='polite') so screen readers read updates after analysis. Avoid color-only sentiment indication — pair red/green with text labels (Positive/Negative/Neutral). Keep buttons as native button for keyboard support. Test with VoiceOver and NVDA.

🔗 Related snippets

🌐 External resources

← All CSS Snippets