Ever wondered why your terminal output looks misaligned when using emojis, CJK characters, or ANSI colors? string-width-ts solves this by accurately calculating the visual width of strings - how many columns they actually occupy on screen.
// ❌ This looks wrong:
console.log("Name".padEnd(10) + "Age");
console.log("古川".padEnd(10) + "25"); // Misaligned!
// ✅ This looks perfect:
import { padString } from "string-width-ts";
console.log(padString("Name", 10) + "Age");
console.log(padString("古川", 10) + "25"); // Perfectly aligned!npm install string-width-tsimport { stringWidth } from "string-width-ts";
// Regular characters
stringWidth("Hello"); // → 5
// Wide characters (CJK)
stringWidth("こんにちは"); // → 10 (each character = 2 columns)
// Emojis
stringWidth("Hello 👋"); // → 7 (emoji = 2 columns)
// ANSI codes (ignored by default)
stringWidth("\x1b[31mRed text\x1b[0m"); // → 8 (ANSI codes don't affect width)| Feature | string-width | wcwidth | string-width-ts |
|---|---|---|---|
| TypeScript Native | ❌ | ✅ Built with TS | |
| Zero Dependencies | ❌ | ❌ | ✅ 100% self-contained |
| Modern ES Features | ❌ | ❌ | ✅ ES2023 & latest TS |
| Enhanced Emoji Support | ❌ | ✅ Complex emoji sequences | |
| Multi-line Support | ❌ | ❌ | ✅ Built-in utilities |
| Detailed Analysis | ❌ | ❌ | ✅ Comprehensive info |
| String Manipulation | ❌ | ❌ | ✅ Truncate, pad, align |
| Custom Width Rules | ❌ | ❌ | ✅ Configurable mapping |
- 🎯 Pixel-Perfect Accuracy: Handles fullwidth, emojis, combining characters, and ANSI codes
- 🌍 Complete Unicode Support: East Asian characters, complex scripts, modern emojis
- 🎨 ANSI-Aware: Properly handles terminal colors and formatting
- 📊 Detailed Analytics: Get comprehensive string composition analysis
- 🔧 Highly Configurable: Custom width rules, normalization, edge case handling
- ⚡ Zero Dependencies: No external packages required
- 🛡️ Type Safe: Full TypeScript support with strict typing
- 🧪 Battle Tested: Comprehensive test suite with 28+ test cases
Calculate the visual width of a string.
import { stringWidth } from "string-width-ts";
// Examples
stringWidth("a"); // → 1
stringWidth("古"); // → 2
stringWidth("👨👩👧👦"); // → 2 (family emoji)
stringWidth("\u001B[1m古\u001B[22m"); // → 2 (ignores ANSI)
// With options
stringWidth("🌈", { emojiAsNarrow: true }); // → 1 instead of 2
stringWidth("古", { ambiguousIsNarrow: false }); // → 2 (wide mode)Get detailed analysis of string composition.
import { getStringWidthInfo } from "string-width-ts";
const info = getStringWidthInfo("Hello 👋 世界");
console.log(info);
// {
// width: 11, // Total visual width
// characters: 9, // Character count
// graphemes: 9, // Grapheme clusters
// ansiSequences: 0, // ANSI escape codes
// emojis: 1, // Emoji count
// zeroWidthChars: 0, // Zero-width characters
// combiningChars: 0, // Combining characters
// widthBreakdown: {
// width0: 0, // Zero-width chars
// width1: 7, // Narrow chars
// width2: 2 // Wide chars
// }
// }Find the width of the widest line in multi-line text.
import { getWidestLineWidth } from "string-width-ts";
const text = `
Hello World
こんにちは 👋
Short
`;
getWidestLineWidth(text); // → 12 (from "こんにちは 👋")Comprehensive analysis for multi-line strings.
import { getMultiLineWidthInfo } from "string-width-ts";
const info = getMultiLineWidthInfo("Hello\n世界\nTest 👋");
console.log(info);
// {
// lines: [
// { content: "Hello", width: 5, /* ... */ },
// { content: "世界", width: 4, /* ... */ },
// { content: "Test 👋", width: 7, /* ... */ }
// ],
// maxWidth: 7, // Widest line
// lineCount: 3, // Number of lines
// averageWidth: 5.33 // Average width
// }Intelligently truncate strings to fit specific widths.
import { truncateString } from "string-width-ts";
// Basic truncation
truncateString("Hello World", 8); // → "Hello..."
// With wide characters
truncateString("Hello 世界", 6); // → "Hel..."
// Custom suffix
truncateString("Hello World", 8, {}, "…"); // → "Hello W…"
// Preserve words
truncateString("Hello beautiful world", 10, { preserveWords: true }); // → "Hello..."Pad strings to specific visual widths.
import { padString } from "string-width-ts";
// Right padding (default)
padString("世界", 6); // → "世界 "
// Left padding
padString("世界", 6, {}, " ", "start"); // → " 世界"
// Center padding
padString("世界", 8, {}, " ", "both"); // → " 世界 "
// Custom padding character
padString("Hello", 10, {}, "-"); // → "Hello-----"interface StringWidthOptions {
// Treat ambiguous characters as narrow (1) instead of wide (2)
ambiguousIsNarrow?: boolean; // default: true
// Include ANSI escape codes in width calculation
countAnsiEscapeCodes?: boolean; // default: false
// Include zero-width characters in calculation
includeZeroWidth?: boolean; // default: false
// Treat emojis as narrow (width 1) instead of wide (width 2)
emojiAsNarrow?: boolean; // default: false
// Custom width rules for specific Unicode code points
customWidthMap?: Map<number, number>;
// Unicode normalization before calculation
normalize?: boolean | "NFC" | "NFD" | "NFKC" | "NFKD"; // default: false
}import { stringWidth, padString } from "string-width-ts";
const data = [
{ name: "John", city: "New York", emoji: "🇺🇸" },
{ name: "田中", city: "東京", emoji: "🇯🇵" },
{ name: "José", city: "Madrid", emoji: "🇪🇸" },
];
// Calculate column widths
const nameWidth = Math.max(...data.map((row) => stringWidth(row.name))) + 2;
const cityWidth = Math.max(...data.map((row) => stringWidth(row.city))) + 2;
// Print perfectly aligned table
data.forEach((row) => {
const line = [
padString(row.name, nameWidth),
padString(row.city, cityWidth),
row.emoji,
].join(" | ");
console.log(line);
});import { stringWidth, truncateString } from "string-width-ts";
function createProgressBar(
label: string,
progress: number,
totalWidth: number
) {
const progressChars = Math.floor((progress / 100) * 20);
const bar = "█".repeat(progressChars) + "░".repeat(20 - progressChars);
const availableWidth = totalWidth - 30; // Reserve space for bar and percentage
const truncatedLabel = truncateString(label, availableWidth);
return `${truncatedLabel} [${bar}] ${progress.toFixed(1)}%`;
}
console.log(createProgressBar("Processing 大きなファイル.txt", 75.5, 60));
// → "Processing 大きなファイ... [███████████████░░░░░] 75.5%"import { stringWidth, getStringWidthInfo } from "string-width-ts";
function wrapText(text: string, maxWidth: number): string[] {
const words = text.split(" ");
const lines: string[] = [];
let currentLine = "";
for (const word of words) {
const testLine = currentLine ? `${currentLine} ${word}` : word;
if (stringWidth(testLine) <= maxWidth) {
currentLine = testLine;
} else {
if (currentLine) lines.push(currentLine);
currentLine = word;
}
}
if (currentLine) lines.push(currentLine);
return lines;
}
const text = "Hello 世界 this is a long text with 絵文字 👋 and symbols";
const wrapped = wrapText(text, 20);
console.log(wrapped);
// Each line fits within 20 visual columnsimport { getStringWidthInfo } from "string-width-ts";
// Complex emoji sequences
const family = "👨👩👧👦"; // Family emoji with ZWJ sequences
const flag = "🇺🇸"; // Flag emoji (regional indicators)
const skinTone = "👋🏽"; // Emoji with skin tone modifier
const familyInfo = getStringWidthInfo(family);
console.log(
`Family emoji: width=${familyInfo.width}, graphemes=${familyInfo.graphemes}`
);
// → Family emoji: width=2, graphemes=1
const flagInfo = getStringWidthInfo(flag);
console.log(
`Flag emoji: width=${flagInfo.width}, characters=${flagInfo.characters}`
);
// → Flag emoji: width=2, characters=2import { stringWidth } from "string-width-ts";
// Define custom width rules
const customMap = new Map([
[65, 3], // 'A' takes 3 columns
[0x1f600, 1], // 😀 takes 1 column instead of 2
[0x4e00, 3], // 一 (CJK) takes 3 columns
]);
const result = stringWidth("A😀一B", { customWidthMap: customMap });
console.log(result); // → 8 (3+1+3+1)import { stringWidth } from "string-width-ts";
// These look the same but are different Unicode sequences
const precomposed = "é"; // Single character U+00E9
const decomposed = "e\u0301"; // e + combining acute accent
console.log(stringWidth(precomposed, { normalize: "NFD" })); // Normalize to decomposed
console.log(stringWidth(decomposed, { normalize: "NFC" })); // Normalize to composed
// Useful for consistent width calculation across different input sources# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Build the project
npm run build
# Development mode (watch for changes)
npm run dev
# Type checking only
npm run lint- Node.js: 18.0.0 or higher
- TypeScript: 5.9.2 (latest) for development
- Zero runtime dependencies 🎉
We welcome contributions! Please see our contributing guidelines for details.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Check out our YouTube channel for in-depth tutorials and examples!
If this project helps you, consider buying me a coffee!
- sindresorhus/string-width - The original inspiration
- timoxley/wcwidth - C-style wcwidth implementation
- komagata/eastasianwidth - East Asian Width property
MIT © ReactJS BD
number: The visual width of the string
interface StringWidthOptions {
// Count ambiguous characters as narrow (1) instead of wide (2)
ambiguousIsNarrow?: boolean; // default: true
// Whether to count ANSI escape codes in width calculation
countAnsiEscapeCodes?: boolean; // default: false
// Whether to include zero-width characters
includeZeroWidth?: boolean; // default: false
// Treat emojis as narrow (width 1) instead of wide (width 2)
emojiAsNarrow?: boolean; // default: false
// Custom width mapping for specific characters
customWidthMap?: Map<number, number>;
// Normalize Unicode before calculation
normalize?: boolean | "NFC" | "NFD" | "NFKC" | "NFKD"; // default: false
}Get detailed information about string width calculation.
import { getStringWidthInfo } from "string-width-ts";
const info = getStringWidthInfo("Hello 👋 世界");
console.log(info);
// {
// width: 11,
// characters: 9,
// graphemes: 9,
// ansiSequences: 0,
// emojis: 1,
// zeroWidthChars: 0,
// combiningChars: 0,
// widthBreakdown: {
// width0: 0, // zero-width characters
// width1: 7, // narrow characters
// width2: 2 // wide characters
// }
// }Get the width of the widest line in a multi-line string.
import { getWidestLineWidth } from "string-width-ts";
const width = getWidestLineWidth("Hello\n世界\nTest");
console.log(width); // 5Get comprehensive width analysis for multi-line strings.
import { getMultiLineWidthInfo } from "string-width-ts";
const info = getMultiLineWidthInfo("Hello\n世界\nTest 👋");
console.log(info);
// {
// lines: [...], // detailed info for each line
// maxWidth: 7, // width of widest line
// lineCount: 3, // number of lines
// averageWidth: 5.33 // average width across lines
// }Truncate a string to a specific visual width.
import { truncateString } from "string-width-ts";
truncateString("Hello World", 8); // 'Hello...'
truncateString("Hello 世界", 6); // 'Hel...'
truncateString("Hello World", 8, {}, "…"); // 'Hello W…'Pad a string to a specific visual width.
import { padString } from "string-width-ts";
padString("世界", 6); // '世界 '
padString("世界", 6, {}, " ", "start"); // ' 世界'
padString("世界", 6, {}, " ", "both"); // ' 世界 'import { stringWidth, padString } from "string-width-ts";
// Create aligned columns
const items = ["Item", "项目", "アイテム", "Element"];
const maxWidth = Math.max(...items.map((item) => stringWidth(item)));
items.forEach((item) => {
console.log(padString(item, maxWidth + 2) + "| Description");
});import { stringWidth, truncateString } from "string-width-ts";
function createProgressBar(label: string, progress: number, width: number) {
const availableWidth = width - 10; // Reserve space for percentage
const truncatedLabel = truncateString(label, availableWidth);
const padding = " ".repeat(availableWidth - stringWidth(truncatedLabel));
return `${truncatedLabel}${padding} ${progress.toFixed(1)}%`;
}
console.log(createProgressBar("Processing 文件.txt", 75.5, 50));import { getMultiLineWidthInfo, padString } from "string-width-ts";
function centerText(text: string, containerWidth: number): string {
const info = getMultiLineWidthInfo(text);
return info.lines
.map((line) => padString("", containerWidth, {}, " ", "both"))
.join("\n");
}| Feature | string-width | string-width-ts |
|---|---|---|
| TypeScript Support | ✅ (types included) | ✅ (built with TS) |
| Basic Width Calculation | ✅ | ✅ |
| ANSI Escape Handling | ✅ | ✅ |
| Emoji Support | ✅ | ✅ Enhanced |
| Detailed Analysis | ❌ | ✅ |
| Multi-line Utilities | ❌ | ✅ |
| String Manipulation | ❌ | ✅ |
| Custom Width Mapping | ❌ | ✅ |
| Unicode Normalization | ❌ | ✅ |
| Zero Dependencies | ❌ | ✅ |
| Modern ES Features | ❌ | ✅ |
import { stringWidth } from "string-width-ts";
// Define custom widths for specific characters
const customMap = new Map([
[65, 3], // 'A' has width 3
[0x1f600, 1], // 😀 has width 1 instead of 2
]);
stringWidth("A😀B", { customWidthMap: customMap }); // 5 (3+1+1)import { stringWidth } from "string-width-ts";
const str1 = "é"; // Precomposed
const str2 = "e\u0301"; // Decomposed (e + combining acute)
stringWidth(str1, { normalize: "NFD" }); // Normalize to decomposed
stringWidth(str2, { normalize: "NFC" }); // Normalize to composedimport { getStringWidthInfo } from "string-width-ts";
// Family emoji with zero-width joiners
const family = "👨👩👧👦";
const info = getStringWidthInfo(family);
console.log(`Width: ${info.width}`); // 2
console.log(`Graphemes: ${info.graphemes}`); // 1
console.log(`Characters: ${info.characters}`); // 11npm test # Run all tests
npm run test:watch # Run tests in watch modenpm run build # Build TypeScript to JavaScript
npm run dev # Build in watch mode- Node.js 16.0.0 or higher
- TypeScript 5.0.0 or higher (for development)
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
MIT © Your Name
- Inspired by sindresorhus/string-width
- Built with modern TypeScript and enhanced features
- Thanks to the Unicode Consortium for character width specifications
- string-width - The original package
- wcwidth - C-style wcwidth implementation
- eastasianwidth - East Asian Width property