Skip to content

The definitive Visual Width calculator. Perfect alignment for Emojis, CJK, and ANSI codes.

License

Notifications You must be signed in to change notification settings

noorjsdivs/string-width-ts

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

1 Commit
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐Ÿ“ string-width-ts

npm version TypeScript License: MIT Node.js Version

YouTube Buy Me a Coffee

A modern, TypeScript-first string width calculator with enhanced Unicode, emoji, and ANSI support


๐ŸŽฏ What is this?

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!

๐Ÿš€ Quick Start

Installation

npm install string-width-ts

Basic Usage

import { 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)

โœจ Why Choose string-width-ts?

๐Ÿ†š Comparison with Alternatives

Feature string-width wcwidth string-width-ts
TypeScript Native โš ๏ธ Types only โŒ โœ… Built with TS
Zero Dependencies โŒ โŒ โœ… 100% self-contained
Modern ES Features โŒ โŒ โœ… ES2023 & latest TS
Enhanced Emoji Support โš ๏ธ Basic โŒ โœ… Complex emoji sequences
Multi-line Support โŒ โŒ โœ… Built-in utilities
Detailed Analysis โŒ โŒ โœ… Comprehensive info
String Manipulation โŒ โŒ โœ… Truncate, pad, align
Custom Width Rules โŒ โŒ โœ… Configurable mapping

๐ŸŽฏ Key Features

  • ๐ŸŽฏ 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

๐Ÿ“– Complete API Reference

Core Functions

stringWidth(string, options?)

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)

getStringWidthInfo(string, options?)

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
//   }
// }

Multi-line Support

getWidestLineWidth(string, options?)

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 "ใ“ใ‚“ใซใกใฏ ๐Ÿ‘‹")

getMultiLineWidthInfo(string, options?)

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
// }

String Manipulation

truncateString(string, targetWidth, options?, suffix?)

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..."

padString(string, targetWidth, options?, padString?, position?)

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-----"

โš™๏ธ Configuration Options

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
}

๐ŸŽฏ Real-World Examples

1. Terminal Tables with Perfect Alignment

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);
});

2. CLI Progress Bars with Unicode

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%"

3. Smart Text Wrapping

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 columns

4. Advanced Emoji Handling

import { 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=2

๐Ÿ”ง Advanced Configuration

Custom Width Mapping

import { 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)

Unicode Normalization

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

๐Ÿงช Testing & Development

# 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

๐Ÿ“‹ Requirements

  • Node.js: 18.0.0 or higher
  • TypeScript: 5.9.2 (latest) for development
  • Zero runtime dependencies ๐ŸŽ‰

๐Ÿค Contributing

We welcome contributions! Please see our contributing guidelines for details.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

๐Ÿ“š Learn More

๐Ÿ“บ Video Tutorials

Check out our YouTube channel for in-depth tutorials and examples!

โ˜• Support the Project

If this project helps you, consider buying me a coffee!

๐Ÿ”— Related Projects

๐Ÿ“œ License

MIT ยฉ ReactJS BD


Made with โค๏ธ using TypeScript 5.9.2

GitHub stars GitHub forks

Returns

  • number: The visual width of the string

Options

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
}

getStringWidthInfo(string, options?)

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
//   }
// }

getWidestLineWidth(string, options?)

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); // 5

getMultiLineWidthInfo(string, options?)

Get 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
// }

truncateString(string, targetWidth, options?, suffix?)

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โ€ฆ'

padString(string, targetWidth, options?, padString?, position?)

Pad a string to a specific visual width.

import { padString } from "string-width-ts";

padString("ไธ–็•Œ", 6); // 'ไธ–็•Œ  '
padString("ไธ–็•Œ", 6, {}, " ", "start"); // '  ไธ–็•Œ'
padString("ไธ–็•Œ", 6, {}, " ", "both"); // ' ไธ–็•Œ '

๐ŸŽฏ Use Cases

Terminal Applications

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");
});

CLI Progress Bars

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));

Text Layout

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");
}

๐Ÿ†š Comparison with string-width

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 โŒ โœ…

๐Ÿ”ง Advanced Usage

Custom Width Mapping

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)

Unicode Normalization

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 composed

Working with Complex Emojis

import { 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}`); // 11

๐Ÿงช Testing

npm test        # Run all tests
npm run test:watch  # Run tests in watch mode

๐Ÿ— Building

npm run build   # Build TypeScript to JavaScript
npm run dev     # Build in watch mode

๐Ÿ“‹ Requirements

  • Node.js 16.0.0 or higher
  • TypeScript 5.0.0 or higher (for development)

๐Ÿค Contributing

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.

๐Ÿ“œ License

MIT ยฉ Your Name

๐Ÿ™ Acknowledgments

  • Inspired by sindresorhus/string-width
  • Built with modern TypeScript and enhanced features
  • Thanks to the Unicode Consortium for character width specifications

๐Ÿ“š Related

About

The definitive Visual Width calculator. Perfect alignment for Emojis, CJK, and ANSI codes.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors