BytePane

Character Counter: Count Characters, Words & Sentences Online

Text Tools14 min read

Key Takeaways

  • Twitter/X standard accounts are limited to 280 characters per post — links are always shortened to 23 characters regardless of URL length, per X's 2026 platform specifications.
  • Google truncates meta descriptions beyond 155–160 characters on desktop and ~120 on some mobile layouts — always write your CTA within the first 155 characters.
  • SMS segments drop from 160 to 70 characters the moment a single non-GSM-7 character appears — one emoji converts the entire message to UCS-2 encoding.
  • JavaScript's .length counts UTF-16 code units, not visible characters — the family emoji 👨‍👩‍👧‍👦 reports .length === 11. Use Intl.Segmenter for accurate UI counters.
  • Per Grammarly's writing research, character count is among the most-used text metrics globally — driven by social media character limits, SEO constraints, and database column validation.

The Tweet That Got Cut Off

Picture this: you schedule a product announcement tweet at 9 PM and wake up to find Twitter truncated your carefully crafted call to action with an ellipsis. The link — buried in the truncated portion — never got clicked. This happens because the author counted characters in a plain text editor that does not emulate Twitter's counting logic, and the numbers disagreed by exactly 4 characters.

Character limits are load-bearing constraints in a developer's world. They govern form validation, database column sizes, SMS gateway costs, SEO snippet rendering, notification payloads, and social media publishing. Getting the count wrong has real consequences — from truncated copy to UCS-2 SMS charges that cost double what you budgeted.

BytePane's character and word counter runs entirely in your browser — paste any text and get an instant breakdown of characters (with and without spaces), words, sentences, lines, and reading time. No data sent to any server. This guide goes deeper: why platform limits differ, how encoding affects the count, and the exact JavaScript code you need for production character counters.

Platform Character Limits: The Definitive 2026 Reference

Different platforms apply character limits for different reasons — UX, storage, API design, legacy protocol constraints. Here is a comprehensive reference based on official 2026 platform specifications:

Platform / FieldLimitEncodingNotes
X (Twitter) — Post280 charsUnicodeLinks → 23 chars. X Premium: 25,000
X — Bio160 charsUnicodeDisplay name: 50 chars
LinkedIn — Post3,000 charsUnicodeConnection request: 300. InMail: 2,000
Instagram — Caption2,200 charsUnicodeOnly first 125 shown before "more"
Google — Title Tag~50–60 charsUnicodePixel-based: ~600px. Chars vary by font.
Google — Meta Description155–160 charsUnicodeMobile may truncate at ~120
SMS — GSM-7160 chars/segmentGSM-7Multi-part: 153 chars per segment
SMS — UCS-2 (emoji)70 chars/segmentUCS-2Multi-part: 67 chars per segment
WhatsApp — Message65,536 charsUTF-8Status (Stories): 700 chars
TikTok — Caption2,200 charsUnicodeBio: 80 chars

The most important nuance in this table: Twitter counts characters, not bytes. A single Chinese character like 文 counts as 1 character toward the 280 limit, not 3 bytes (UTF-8) or 2 bytes (UCS-2). This is deliberate — Unicode characters are first-class in Twitter's counting model. SMS, by contrast, uses byte-based encoding where character limits differ by encoding scheme.

The SMS Encoding Trap: Why One Emoji Doubles Your Cost

SMS is the character counting domain with the most financial consequences, and the most confusion. The root cause is encoding: SMS was designed in the 1980s around GSM-7 — a 7-bit encoding with 128 characters covering basic Latin, numbers, and a few special symbols. A GSM-7 message fits 160 characters in a single 140-byte segment.

The problem: GSM-7 does not include emoji, curly quotes (" "), em dashes (—), or most non-Latin characters. The moment your message contains any character outside GSM-7, the SMS gateway switches the entire message to UCS-2 (a 16-bit encoding). The per-segment capacity immediately drops from 160 to 70 characters.

SMS character counting — the emoji encoding trap
// GSM-7 character set (simplified)
const GSM7_CHARS = /^[A-Za-z0-9 \r\n@£$¥èéùìòÇØøÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ!"#%&'()*+,\-./:;<=>?¡ÄÖÑܧ¿äöñüà^{}\\\[~\]|€]*$/

function smsInfo(text) {
  const isGSM7 = GSM7_CHARS.test(text)
  const charCount = text.length

  if (isGSM7) {
    const segments = charCount <= 160 ? 1 : Math.ceil(charCount / 153)
    return { encoding: 'GSM-7', chars: charCount, segmentSize: 160, segments }
  } else {
    // UCS-2: emoji, curly quotes, em dash, etc.
    const ucsCount = [...text].length  // Unicode code points
    const segments = ucsCount <= 70 ? 1 : Math.ceil(ucsCount / 67)
    return { encoding: 'UCS-2', chars: ucsCount, segmentSize: 70, segments }
  }
}

smsInfo("Hello, world! Great news.")
// { encoding: 'GSM-7', chars: 25, segmentSize: 160, segments: 1 }

smsInfo("Hello! 🎉 Great news.")
// { encoding: 'UCS-2', chars: 21, segmentSize: 70, segments: 1 }
// NOTE: 21 chars costs the same as 70 — you "waste" 49 characters of capacity

smsInfo("A".repeat(155))
// { encoding: 'GSM-7', chars: 155, segmentSize: 160, segments: 1 }

smsInfo("A".repeat(161))
// { encoding: 'GSM-7', chars: 161, segmentSize: 160, segments: 2 }
// Now 2 segments charged — multi-part header uses 7 chars, leaving 153 per segment

The financial implication: a 2-segment GSM-7 message costs 2× a 1-segment message. Add an emoji and you go from 160 chars/segment to 70 chars/segment — meaning a 120-character message that would be 1 GSM-7 segment becomes a 2-segment UCS-2 message when you add a single emoji. This is a well-known pitfall in SMS marketing. Per Twilio's developer documentation, unicode characters are one of the most common causes of unexpectedly high SMS costs.

GSM-7 Extended Characters: The GSM-7 extended table includes , {, }, [, ], ~, |, and \. These are still GSM-7 but count as 2 characters each — an extended table escape byte plus the character. A message with just one sign effectively uses 2 character slots.

Building a Production Character Counter in JavaScript

The apparent simplicity of character counting in JavaScript is a trap. The String.prototype.length property returns UTF-16 code units, not Unicode characters, not grapheme clusters (visible characters). For an English-only form field, it does not matter. For any UI that users can populate with emoji or international text, it matters enormously.

Three ways to count characters — and when to use each
const text = "Hello 👨‍👩‍👧‍👦 World"

// 1. UTF-16 code units — what .length returns
// Use for: database varchar limits (which are often byte/unit based)
text.length  // 18 (family emoji = 11 UTF-16 units)

// 2. Unicode code points — what most humans mean by "characters"
// Use for: Twitter counting, simple display counters
[...text].length         // 9
Array.from(text).length  // 9

// 3. Grapheme clusters — what users see as individual characters
// Use for: accurate UI character counters, text editors
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' })
[...segmenter.segment(text)].length  // 9 (family emoji = 1 visible character)

// Practical: character count without spaces (some platforms use this)
function charsWithoutSpaces(str) {
  return Array.from(str.replace(/\s/g, '')).length
}

// Practical: Twitter-style counter (code points, links = 23)
function twitterCount(text) {
  // Replace URLs with 23-char placeholder
  const normalized = text.replace(/https?:\/\/\S+/g, 'x'.repeat(23))
  return Array.from(normalized).length
}

Real-Time Counter with React

Here is a production-quality character counter component that correctly handles emoji, shows color-coded limits, and updates in real time:

React character counter — handles emoji, color-coded limit feedback
import { useState, useMemo } from 'react'

const MAX_CHARS = 280  // Twitter limit

function CharacterCounter() {
  const [text, setText] = useState('')

  const stats = useMemo(() => {
    const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' })
    const graphemes = [...segmenter.segment(text)]
    const charCount = graphemes.length
    const remaining = MAX_CHARS - charCount
    const percentage = (charCount / MAX_CHARS) * 100

    return {
      chars: charCount,
      words: text.trim() ? text.trim().split(/\s+/).filter(Boolean).length : 0,
      sentences: text.trim() ? (text.match(/[.!?]["')]*(?:\s|$)/g) || [text]).length : 0,
      remaining,
      percentage,
      status: remaining < 0 ? 'over' : remaining < 20 ? 'warning' : 'ok',
    }
  }, [text])

  const statusColor = {
    ok: 'text-green-400',
    warning: 'text-yellow-400',
    over: 'text-red-400',
  }[stats.status]

  return (
    <div className="space-y-3">
      <textarea
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Start typing..."
        rows={4}
        className="w-full p-3 bg-dark-surface border border-dark-border rounded-lg resize-none"
      />
      <div className="flex justify-between text-sm">
        <span className="text-text-muted">
          {stats.words} words · {stats.sentences} sentences
        </span>
        <span className={statusColor}>
          {stats.remaining >= 0
            ? `${stats.chars}/${MAX_CHARS}`
            : `${Math.abs(stats.remaining)} over limit`}
        </span>
      </div>
      {/* Progress bar */}
      <div className="h-1 bg-dark-border rounded-full overflow-hidden">
        <div
          className={`h-full transition-all ${stats.status === 'over' ? 'bg-red-400' : stats.status === 'warning' ? 'bg-yellow-400' : 'bg-green-400'}`}
          style={{ width: `${Math.min(stats.percentage, 100)}%` }}
        />
      </div>
    </div>
  )
}

The Intl.Segmenter API is available in all modern browsers (Chrome 87+, Firefox 78+, Safari 14.1+, Edge 87+) and Node.js 16+. For older environments, the graphemer npm package provides polyfill behavior based on Unicode Annex 29 segmentation rules.

SEO Character Limits: Title Tags and Meta Descriptions

Google does not publish a character limit for title tags — it publishes a pixel width limit of approximately 600 pixels for desktop and 920 pixels for mobile. The character count that stays within the pixel budget varies by font metrics. In practice, Moz's analysis of 89,000 SERPs found that 50–60 characters keeps 95%+ of titles within the rendered threshold using typical Latin glyphs.

Meta descriptions are different: Google caps the snippet at approximately 920 pixels width on desktop, which translates to roughly 155–160 characters. Per research by Portent (analyzing 35,000 search result snippets), descriptions between 150 and 158 characters had the lowest truncation rate. Descriptions over 160 characters were truncated in 97% of cases.

SEO ElementSafe RangeMaximum Before TruncationPixel Budget
Page title tag50–60 chars~70 chars (wide glyphs sooner)~600px desktop
Meta description (desktop)150–158 chars160 chars~920px
Meta description (mobile)120–125 chars~130 charsVaries by device
Open Graph title40–60 charsPlatform-dependentN/A (varies by sharer)

A practical rule: write your meta description CTA within the first 130 characters. The remaining 25–30 characters of budget buffer against mobile truncation. If you are writing titles with brand names (“Tool Name | Brand”), count that suffix against your 60-character budget — Google sometimes rewrites titles that exceed the limit anyway.

For programmatic SEO pages where descriptions are generated from templates, building character count validation into your content pipeline prevents truncated snippets at scale. Check our regex cheat sheet for patterns you can use to validate meta description length in a CI pipeline.

Database Column Limits and Encoding Gotchas

When developers think about character limits in code, they usually think about UI validation. But the deepest consequences of character counting errors surface in database persistence. The difference between a VARCHAR(255) and a character count depends entirely on the database's charset configuration:

Database encoding and column size — what VARCHAR(255) actually stores
-- MySQL utf8mb4 (correct Unicode support)
-- VARCHAR(255) stores 255 CHARACTERS, up to 4 bytes each = up to 1020 bytes
-- A single emoji (4-byte UTF-8) counts as 1 character toward the 255 limit
CREATE TABLE posts (content VARCHAR(280) CHARACTER SET utf8mb4);

-- MySQL utf8 (legacy — only 3-byte Unicode, emoji breaks!)
-- The 💩 emoji (U+1F4A9) is 4 bytes → INSERT fails silently or errors
-- ALWAYS use utf8mb4, not utf8, in MySQL
CREATE TABLE posts_wrong (content VARCHAR(280) CHARACTER SET utf8);  -- AVOID

-- PostgreSQL VARCHAR(n) — n is CHARACTER count, not byte count
-- Both emoji and CJK chars count as 1 character toward the limit
CREATE TABLE posts (content VARCHAR(280));  -- Always correct for Unicode

-- PostgreSQL TEXT — no limit, always UTF-8
CREATE TABLE posts (content TEXT);

-- Checking actual storage in Postgres
SELECT char_length(content), octet_length(content) FROM posts WHERE id = 1;
-- char_length: Unicode character count (what users see)
-- octet_length: byte count (what the disk stores, 1–4 bytes per char)

The MySQL utf8 charset — distinct from utf8mb4 — only supports Unicode characters up to U+FFFF (3 bytes). This means emoji (which start at U+1F000) are silently truncated or cause errors. The MySQL documentation has acknowledged this as a longstanding design limitation. Per the MySQL 8.0 docs, “Applications that need supplementary character support should use utf8mb4.” The wrong charset is responsible for a significant portion of the “data truncated” errors reported on Stack Overflow MySQL questions.

Character Density and Readability Metrics

Beyond raw counts, character and word data feeds into readability metrics used by content teams, editors, and automated SEO tools. The most commonly referenced is the Flesch Reading Ease score, calculated from syllables, words, and sentences:

Flesch Reading Ease — approximately correct for English prose
// Flesch Reading Ease: 0–100, higher = easier to read
// Formula: 206.835 - 1.015 * (words/sentences) - 84.6 * (syllables/words)
function fleschReadingEase(text) {
  const words = text.trim().split(/\s+/).filter(Boolean).length
  const sentences = (text.match(/[.!?]+/g) || []).length || 1
  const syllables = text
    .toLowerCase()
    .split(/\s+/)
    .reduce((total, word) => {
      // Approximate syllable count: vowel groups, subtract trailing 'e'
      const syllableCount = word
        .replace(/[^aeiou]/gi, '')
        .replace(/e$/, '')
        .length || 1
      return total + syllableCount
    }, 0)

  const score = 206.835 - 1.015 * (words / sentences) - 84.6 * (syllables / words)
  return Math.max(0, Math.min(100, Math.round(score)))
}

// Score interpretation:
// 90–100: Very easy (5th grade)
// 70–80:  Easy (7th grade)
// 60–70:  Standard (8th–9th grade)
// 50–60:  Fairly difficult (10th–12th grade)
// 30–50:  Difficult (college level)
//  0–30:  Very confusing (professional/academic)

According to the Nielsen Norman Group's research on web readability (analyzing 79 usability studies), web users read only 20–28% of text on a page. Short sentences (15–20 words average) and high Flesch scores significantly increase comprehension in web contexts. The U.S. federal government mandates a minimum 60 Flesch score for public-facing documents under the Plain Writing Act of 2010.

For developers building CMS systems or writing assistants, the combination of character count, word count, sentence count, and Flesch score provides a complete real-time writing quality dashboard. Many modern editors (Hemingway Editor, Grammarly, Notion AI) surface exactly these four metrics in their UIs.

If you need to analyze large blocks of text programmatically, our word counter deep-dive covers the algorithmic edge cases — CJK text, emoji Unicode handling, and word-boundary detection for production systems.

Character Counter Tools: What to Look For

The market for online character counting tools ranges from single-purpose counters to full text analysis suites. Here is what separates adequate tools from genuinely useful ones, based on what actually matters in developer and content workflows:

FeatureWhy It MattersBytePaneWordCounter.netCharacterCountOnline
Client-side processingText never sent to server — privacyYesPartialYes
Chars with/without spacesAcademic and ad copy requirementsYesYesYes
Sentence countReading ease calculationsYesYesNo
Reading time estimateBlog/content planningYesYesNo
No account requiredQuick one-off countsYesYesYes
No ads interrupting countFocus when working under deadlineYesNoNo

One critical feature this table does not capture: how a tool handles emoji and non-ASCII input. Many older character counters use JavaScript's .length property, which will report the family emoji 👨‍👩‍👧‍👦 as 11 characters. If you are counting for a Twitter field that treats it as 2 (code points), this discrepancy matters. Test any tool you plan to rely on with input like 👩‍💻🏳️‍🌈 and verify the count matches your platform's actual behavior.

Frequently Asked Questions

What is the character limit for Twitter/X in 2026?

Standard X accounts are limited to 280 characters per tweet. Links are shortened to 23 characters regardless of actual URL length. X Premium subscribers can write up to 25,000 characters. X bios are capped at 160 characters, and display names at 50. One Chinese character counts as 1, not 2, toward the limit — Twitter uses code-point counting, not byte counting.

How many characters should a meta description be?

Google displays approximately 155–160 characters for meta descriptions on desktop. Descriptions over 160 characters are truncated with an ellipsis, typically cutting off your call to action. Mobile search results may truncate earlier (~120 characters). Best practice: write compelling copy within 155 characters and put your CTA within the first 130 for mobile safety.

Does an emoji count as 1 or 2 characters?

It depends on context. JavaScript's .length returns 2 for most emoji (UTF-16 surrogate pairs). Twitter counts code points (usually 2 for emoji above U+FFFF). For true visible-character counts, use Intl.Segmenter which returns 1. The ZWJ family emoji 👨‍👩‍👧‍👦 is visually 1 character but represents 4 code points and 11 UTF-16 units.

How many characters is an SMS text message?

A single SMS in GSM-7 encoding (basic Latin) is 160 characters. Including any emoji or non-GSM-7 character switches the message to UCS-2, dropping the limit to 70 characters per segment. Multi-segment messages use 7 header bytes per segment, leaving 153 chars (GSM-7) or 67 chars (UCS-2) of usable content per segment. Extended GSM-7 characters like € count as 2 characters each.

What is the Google title tag character limit?

Google enforces a pixel width limit (~600px), not a hard character count. In practice, 50–60 characters in typical fonts fits within this budget. Wider glyphs (like W, M, or Chinese characters) consume the pixel budget faster — a 55-character title with all caps or CJK characters may still get truncated while a 62-character lowercase title clears it. Moz's Title Tag Preview Tool tests pixel width directly.

How do I count characters in JavaScript without counting spaces?

Use text.replace(/\s/g, '').length for UTF-16 unit count without spaces. For Unicode-correct counting: Array.from(text.replace(/\s/g, '')).length. The \s pattern matches spaces, tabs, newlines, and other whitespace. Some academic publishers count without spaces to normalize across languages where word lengths vary significantly.

What is the LinkedIn post character limit?

LinkedIn posts are limited to 3,000 characters for personal profiles and Company Pages. LinkedIn articles have no enforced limit (up to ~125,000 characters in practice). Connection request messages: 300 characters. InMail messages: 2,000 characters. LinkedIn ads vary by format — Sponsored Content text is 150 characters; Headlines are 70 characters. LinkedIn bio: 2,600 characters.

Count Characters Instantly

Paste any text — tweets, meta descriptions, SMS drafts, email subjects — and get character count (with and without spaces), word count, sentence count, and reading time. Runs entirely in your browser. No data leaves your device.

Open Character Counter →