Lodash _.pad Issues with Multi-Code Point Emojis
Lodash’s _.pad function relies on standard JavaScript
UTF-16 code units to compute string length rather than actual visual
glyph representations. When strings contain multi-code point emojis—such
as zero-width joiner (ZWJ) sequences, skin-tone modifiers, or country
flags—Lodash miscalculates the visual width of the text. This mismatch
causes under-padding, asymmetrical centering, and even character
corruption if complex emojis are used as the padding characters
themselves.
The Root Cause: Code Units vs. Grapheme Clusters
JavaScript strings measure length using 16-bit code units via
String.prototype.length. A standard alphanumeric character
has a length of 1. However, modern emojis often consist of surrogate
pairs, combining marks, and joiners:
- Surrogate pairs (e.g.,
😀) have a.lengthof 2. - Skin-tone modifiers (e.g.,
👍🏽) combine a base emoji with a color modifier, yielding a.lengthof 4. - ZWJ sequences (e.g.,
👨👩👧👦or👩💻) combine multiple emojis using Zero-Width Joiner characters (\u200D), yielding lengths from 5 to 11. - Regional indicator flags (e.g.,
🇺🇸) use pairs of regional indicator symbols, giving a.lengthof 4.
Because _.pad(string, length, [chars=' ']) determines
required padding by subtracting string.length from the
target length, the function operates on raw code units
rather than user-perceived characters (grapheme clusters).
Specific Formatting Inconsistencies
1. Severe Under-Padding
When formatting text to align in fixed-width displays or terminal
interfaces, _.pad assumes a string with an emoji is
significantly wider than it appears visually. For example, if you
attempt to pad 👩💻 (visual width: 1 or 2 characters; code
unit length: 5) to a total length of 8, Lodash calculates:
\[\text{Padding Needed} = 8 - 5 = 3\]
Instead of providing roughly 6 to 7 spaces of padding to reach an apparent length of 8, it only adds 3 spaces. This breaks column alignment in tables and CLI outputs.
2. Asymmetrical and Skewed Centering
_.pad balances padding by distributing the calculated
deficit between the left and right sides of the string:
// Desired: visual center in an 8-character slot
_.pad('👩💻', 8);
// Result: " 👩💻 " (1 space left, 2 spaces right)Because the remaining space calculation is skewed by the invisible
code units, the visual output fails to be centered and is weighted
unevenly across the sides. If the emoji's code unit length meets or
exceeds the target length, _.pad adds no padding at all,
even if the string visually occupies only one column.
3. Truncated Glyphs and Unicode Corruption
The issue also occurs in reverse when using multi-code point emojis
as the fill argument (chars). If the remaining space
requires an odd number of code units, Lodash truncates the padding
string to fit the target length:
_.pad('test', 7, '😀');Because 😀 requires 2 code units, filling an odd gap
requires splitting the surrogate pair. Lodash slices the string
mid-character, leaving an orphaned high surrogate that renders as an
unprintable replacement character (``). Similarly, truncating a ZWJ
sequence strips modifiers, transforming a combined character into
disconnected components (such as an individual person icon followed by
an isolated laptop symbol).
Resolving the Inconsistency
To ensure consistent formatting with multi-code point emojis, strings must be measured and padded based on grapheme clusters rather than UTF-16 code units:
- Use
Intl.Segmenter: Modern JavaScript environments provideIntl.Segmenterto accurately count user-perceived graphemes:const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' }); const graphemeCount = [...segmenter.segment(str)].length; - Employ Grapheme-Aware Libraries: Utilities like
grapheme-splitterorstring-widthshould be used to calculate true visual lengths (especially accounting for full-width characters in terminal outputs) before applying custom padding logic.