Truncate Text with an Ellipsis
Truncating overflowing text with `...` keeps card layouts and table cells from breaking, but the naive `slice` approach often cuts mid-word or splits a surrogate pair into garbage. This snippet covers the simple character cap, a word-boundary aware version, and a code-point-correct variant for international content. Use it for previews, tooltips, and any space-bounded label.
1,154 views
12
function truncate(str, maxLength, suffix = '...') {
if (typeof str !== 'string') return '';
if (str.length <= maxLength) return str;
return str.slice(0, maxLength - suffix.length) + suffix;
}
console.log(truncate('The quick brown fox jumps', 15));
// The quick br...
console.log(truncate('short', 15));
// short
console.log(truncate('exactly fifteen', 15));
// exactly fifteenThe simplest correct truncate keeps the total output (including suffix) within maxLength by slicing at maxLength - suffix.length. Returning the input unchanged when it already fits is the easy branch to forget; without it, a 10-character string gets truncated to '1234567...' even though it could have been displayed in full. This version counts UTF-16 code units, so a string with emoji or rare-script characters can still be cut mid-character. Reach for this when your inputs are ASCII (English UI labels, technical IDs) and you want one defensive line.
function truncateWords(str, maxLength, suffix = '...') {
if (typeof str !== 'string') return '';
if (str.length <= maxLength) return str;
const cutAt = maxLength - suffix.length;
const sliced = str.slice(0, cutAt);
const lastSpace = sliced.lastIndexOf(' ');
const trimmed = lastSpace > 0 ? sliced.slice(0, lastSpace) : sliced;
return trimmed.replace(/[\s,;:.!?]+$/, '') + suffix;
}
console.log(truncateWords('The quick brown fox jumps over', 20));
// The quick brown...
console.log(truncateWords('Single-very-long-token-here', 15));
// Single-very-...
console.log(truncateWords('Hello, world!', 50));
// Hello, world!Cutting mid-word produces ugly previews like 'The quick brown fo...', so a friendlier helper backs up to the last space inside the budget. The fallback lastSpace > 0 matters because a single super-long token (like a URL or hash) has no space to retreat to, in which case the function falls back to the hard character cut so you do not return an empty string. The trailing replace(/[\s,;:.!?]+$/, '') strips whatever punctuation or whitespace ended the trimmed slice, so the visible result reads 'word...' instead of 'word ,...'. Use it for blog excerpts, search snippets, and any preview where readability beats exact length.
function truncateCodePoints(str, maxCodePoints, suffix = '...') {
if (typeof str !== 'string') return '';
const codePoints = Array.from(str);
if (codePoints.length <= maxCodePoints) return str;
const budget = maxCodePoints - Array.from(suffix).length;
return codePoints.slice(0, Math.max(0, budget)).join('') + suffix;
}
console.log(truncateCodePoints('Hello \u{1F30D} world', 10)); // Hello 🌍... (globe preserved as one code point)
console.log(truncateCodePoints('日本語のテスト文字列', 5)); // 日本...
console.log(truncateCodePoints('abc', 10)); // abcEmoji like '🌍' (U+1F30D) and characters above the Basic Multilingual Plane occupy two UTF-16 code units, so str.length overcounts and str.slice can cut between the surrogate halves to produce garbage like '\uD83C'. Array.from(str) iterates by code point, giving you one entry per user-perceived character (for non-combined input), so slicing the array and rejoining is safe. We also count the suffix in code points to keep the budget honest. Reach for this whenever your strings can contain CJK, Devanagari, emoji, or any non-ASCII content where the naive byte count lies.
