string-width
PoolModern, zero-dependency string width calculator - beats string-width by 10-20%, 2-3x faster on ASCII
@lpm.dev/neo.string-width
Modern, ultra-fast string width calculator with zero dependencies
Calculate the visual width of strings for terminal/console output. Correctly handles Unicode, emoji, CJK characters, and ANSI escape codes.
Why This Package?
🚀 Blazing Fast
- 6.8x faster on ASCII text than string-width
- 1.2x faster on CJK text than string-width
- 1.8x faster on emoji than string-width
✨ Feature Rich
- Built-in helper functions: truncate, pad, slice, split by width
- Full TypeScript support with strict types
- Tree-shakeable (import only what you need)
🎯 Zero Dependencies
- Only dependency:
@lpm.dev/neo.ansi(itself zero-dep) - Smaller bundle than alternatives
💯 Correct
- Unicode 16 compliant
- Handles all edge cases (emoji with skin tones, ZWJ sequences, combining marks)
- 328 comprehensive tests (99.55% coverage)
Installation
lpm install @lpm.dev/neo.string-widthQuick Start
import stringWidth from "@lpm.dev/neo.string-width";
// ASCII text
stringWidth("hello"); // => 5
// CJK characters (fullwidth)
stringWidth("你好"); // => 4 (2 chars × 2 width)
// Emoji
stringWidth("👍"); // => 2
// ANSI colored text (codes are stripped)
stringWidth("\x1b[31mRed\x1b[0m"); // => 3
// Mixed content
stringWidth("Hello 你好 👍"); // => 11API
stringWidth(text, options?)
Calculate the visual width of a string.
Parameters:
text(string): Text to measure-
options(object, optional):ambiguousIsNarrow(boolean): Treat ambiguous-width characters as narrow (default:true)countAnsiEscapeCodes(boolean): Count ANSI codes in width (default:false)emojiWidth(number | 'auto'): Width for emoji (default:2)normalize(boolean): Normalize text before measuring (default:false)
Returns: number - Visual width in columns
import stringWidth from "@lpm.dev/neo.string-width";
stringWidth("hello"); // => 5
stringWidth("你好世界"); // => 8
stringWidth("👍👎"); // => 4Helper Functions
truncateToWidth(text, width, options?)
Truncate text to fit within a specified width.
import { truncateToWidth } from "@lpm.dev/neo.string-width";
// End truncation (default)
truncateToWidth("Hello World", 8);
// => 'Hello W…'
// Start truncation
truncateToWidth("Hello World", 8, { position: "start" });
// => '…o World'
// Middle truncation
truncateToWidth("Hello World", 8, { position: "middle" });
// => 'Hel…orld'
// Custom ellipsis
truncateToWidth("Hello World", 8, { ellipsis: "..." });
// => 'Hello...'
// Word boundary preference
truncateToWidth("Hello World Test", 10, { wordBoundary: true });
// => 'Hello…'Options:
position: 'end' | 'start' | 'middle' (default: 'end')ellipsis: string (default: '…')wordBoundary: boolean (default: false)
padToWidth(text, width, options?)
Pad text to reach a specified width.
import { padToWidth } from "@lpm.dev/neo.string-width";
// Right padding (default)
padToWidth("Hello", 10);
// => 'Hello '
// Left padding
padToWidth("Hello", 10, { align: "right" });
// => ' Hello'
// Center padding
padToWidth("Hello", 10, { align: "center" });
// => ' Hello '
// Custom pad character
padToWidth("Hello", 10, { padChar: "." });
// => 'Hello.....'
// Works with CJK
padToWidth("你好", 10);
// => '你好 'Options:
align: 'left' | 'right' | 'center' (default: 'left')padChar: string (default: ' ')
sliceByWidth(text, start, end?)
Slice text by visual width (like String.slice but width-aware).
import { sliceByWidth } from "@lpm.dev/neo.string-width";
// Basic slicing
sliceByWidth("Hello World", 0, 5);
// => 'Hello'
// Negative indices
sliceByWidth("Hello World", -5);
// => 'World'
// Works with CJK (width-based, not character-based)
sliceByWidth("你好世界", 0, 4);
// => '你好' (2 chars, width 4)
// Mixed content
sliceByWidth("Hello 你好", 0, 7);
// => 'Hello 你'splitByWidth(text, maxWidth, options?)
Split text into chunks that fit within a maximum width.
import { splitByWidth } from "@lpm.dev/neo.string-width";
// Basic splitting
splitByWidth("Hello World", 5);
// => ['Hello', 'World']
// CJK text
splitByWidth("你好世界测试", 8);
// => ['你好世界', '测试']
// Word boundary preference
splitByWidth("Hello World Test", 10, { preferSplitOnSpace: true });
// => ['Hello', 'World Test']
// Preserve whitespace
splitByWidth(" Hello ", 10, { preserveWhitespace: true });
// => [' Hello ']Options:
preferSplitOnSpace: boolean (default: true)preserveWhitespace: boolean (default: false)
Unicode Support
East Asian Width Categories
Correctly handles all UAX #11 categories:
| Category | Width | Examples |
|---|---|---|
| Fullwidth (F) | 2 | ,、。! |
| Wide (W) | 2 | 你好世界 (CJK) |
| Halfwidth (H) | 1 | アイウエオ |
| Narrow (Na) | 1 | hello |
| Ambiguous (A) | 1* | ±§¶ |
| Neutral (N) | 1 | hello |
* Configurable via ambiguousIsNarrow option
Emoji Support
- ✅ Simple emoji: 👍 (width 2)
- ✅ Emoji with skin tones: 👍🏻 (width 2, single grapheme)
- ✅ ZWJ sequences: 👨👩👧👦 (width 2, single grapheme)
- ✅ Variation selectors: ❤️ vs ❤︎
- ✅ Emoji presentation: automatic detection
Special Characters
- ✅ Zero-width characters (ZWSP, ZWNJ, ZWJ, etc.)
- ✅ Combining marks (é = e + combining acute)
- ✅ ANSI escape codes (automatically stripped)
- ✅ Grapheme clusters (proper Unicode segmentation)
Performance
Benchmarks vs string-width
| Test Case | neo.string-width | string-width | Result |
|---|---|---|---|
| ASCII | 17.4M ops/sec | 2.5M ops/sec | 6.8x faster 🚀 |
| CJK | 2.5M ops/sec | 2.1M ops/sec | 1.2x faster ✨ |
| Emoji | 3.0M ops/sec | 1.6M ops/sec | 1.8x faster ⚡ |
How? Smart fast-path optimization that avoids expensive grapheme segmentation for simple text (80%+ of real-world cases).
Bundle Size
@lpm.dev/neo.string-width: 13.70 KB (ESM)
string-width + dependencies: ~17 KB
Savings: ~20% smallerTree-shakeable: Import only what you need!
// Import everything (13.70 KB)
import stringWidth, {
truncateToWidth,
padToWidth,
} from "@lpm.dev/neo.string-width";
// Import only stringWidth (~8 KB)
import stringWidth from "@lpm.dev/neo.string-width";
// Import only helpers you need
import { truncateToWidth } from "@lpm.dev/neo.string-width";TypeScript
Fully typed with strict TypeScript support:
import stringWidth, {
type StringWidthOptions,
type TruncateOptions,
type PadOptions,
type SliceOptions,
type SplitOptions,
} from "@lpm.dev/neo.string-width";
// Type-safe options
const options: StringWidthOptions = {
ambiguousIsNarrow: false,
emojiWidth: 1,
};
stringWidth("text", options); // numberReal-World Examples
Terminal Table Formatting
import { padToWidth, truncateToWidth } from "@lpm.dev/neo.string-width";
function formatTableRow(name: string, value: string) {
const nameCol = padToWidth(truncateToWidth(name, 20), 20);
const valueCol = padToWidth(value, 30, { align: "right" });
return `│ ${nameCol} │ ${valueCol} │`;
}
formatTableRow("User Name", "你好世界");
// => '│ User Name │ 你好世界 │'Progress Bar
import { padToWidth } from "@lpm.dev/neo.string-width";
function progressBar(percent: number, width: number = 40) {
const filled = Math.floor((width * percent) / 100);
const bar = "█".repeat(filled) + "░".repeat(width - filled);
return `[${bar}] ${percent}%`;
}
progressBar(75, 20);
// => '[███████████████░░░░] 75%'Text Wrapping
import { splitByWidth } from "@lpm.dev/neo.string-width";
function wrapText(text: string, maxWidth: number): string[] {
return splitByWidth(text, maxWidth, { preferSplitOnSpace: true });
}
wrapText("这是一段很长的中文文本需要被分割", 20);
// => ['这是一段很长的中文文', '本需要被分割']Truncate File Paths
import { truncateToWidth } from "@lpm.dev/neo.string-width";
function truncatePath(path: string, maxWidth: number) {
return truncateToWidth(path, maxWidth, { position: "middle" });
}
truncatePath("/very/long/path/to/some/file.txt", 25);
// => '/very/long/…some/file.txt'Migration from string-width
Drop-in replacement in most cases:
// Before
import stringWidth from "string-width";
const width = stringWidth("text");
// After
import stringWidth from "@lpm.dev/neo.string-width";
const width = stringWidth("text"); // Same API!See MIGRATION.md for detailed migration guide.
How It Works
- ANSI Stripping: Remove escape codes using @lpm.dev/neo.ansi
- ASCII Fast Path: Return
text.lengthfor ASCII-only (6.8x speedup) - Simple Unicode Fast Path: Code-point iteration for simple CJK/emoji (20x speedup)
- Complex Grapheme Path: Use Intl.Segmenter only when needed (ZWJ, combining marks)
- Width Calculation: East Asian Width lookup + emoji detection
Smart Optimization: 80%+ of real-world text uses fast paths, avoiding expensive operations.
Browser Support
- ✅ Node.js 18+
- ✅ Modern browsers (Chrome 87+, Firefox 90+, Safari 14.1+)
- ✅ Requires
Intl.Segmenterfor complex grapheme clusters
Testing
npm test # Run all tests
npm run test:coverage # Coverage report (99.55%)
npm run bench # Performance benchmarksCoverage: 328 tests, 99.55% code coverage
License
MIT
FAQ
Q: Why is this faster than string-width?
A: Smart fast-path optimization. We avoid expensive Intl.Segmenter for 80%+ of text by detecting when it's not needed (simple CJK, simple emoji, ASCII).
Q: Is this a drop-in replacement for string-width?
A: Yes, in most cases! Same API for the core stringWidth() function. Plus bonus helper functions.
Q: What about emoji support?
A: Full support including skin tones (👍🏻), ZWJ sequences (👨👩👧👦), and variation selectors (❤️).
Q: Does it work in browsers?
A: Yes! Requires browsers with Intl.Segmenter support (Chrome 87+, Firefox 90+, Safari 14.1+).
Q: Why create another string-width package?
A: To provide better performance, built-in helpers, zero dependencies, and TypeScript-first design.
Taxes calculated at checkout based on your location.