CSS ellipsis is great.
Until the string itself matters.
It works when you only need to visually hide overflow.
A long label.
A table cell.
A title inside a card.
Done.
But sometimes your app needs more than a clipped box.
Sometimes it needs the actual truncated string.
Sometimes it needs to preserve a match.
Sometimes it needs to cut from the middle.
Sometimes it needs to prepare the text before the UI renders.
That is why I built Truncate.
A small Pretext powered library for all those cases where CSS ellipsis is not enough.
Even the weird ones.
Try it here:
Playground:
https://tonyblu331.github.io/truncate/
Repo:
https://github.com/tonyblu331/truncate
The problem
Most truncation starts here:
.label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
And honestly, that is still the right answer for a lot of UI.
CSS ellipsis does not fail.
It does exactly what it is meant to do.
It clips text visually.
But it does not give you the final string.
It does not know which part of the text matters.
It does not know that a file extension should stay visible.
It does not know that a search result should keep the match in view.
It does not help when you need the output before rendering.
It just clips.
That is the gap Truncate is trying to cover.
What Truncate does
Truncate gives you small utilities for text truncation before it reaches the UI.
It supports:
- width based truncation
- multiline truncation
- middle cuts
- start cuts
- search aware truncation
- range aware truncation
- custom markers
- height aware truncation
- grapheme safe text
- DOM free usage
The point is not to replace CSS.
CSS is still the right tool when the browser only needs to hide overflow.
Truncate is for the cases where your app needs to know what the final text should be.
Basic usage
import { truncate } from "@tonybonet/truncate";
truncate("A very long string", {
font: "16px Inter",
maxWidth: 100,
});
Simple.
But the useful part is when the case gets more specific.
Middle truncation
Some strings should not only be cut from the end.
Filenames.
URLs.
Emails.
Hashes.
IDs.
Anything where the start and end both matter.
import { truncateMiddle } from "@tonybonet/truncate";
truncateMiddle("really-long-file-name-final-final-v12.pdf", {
font: "14px Inter",
maxWidth: 220,
});
Because this:
really-long-file-name...
is not always as useful as this:
really-long...v12.pdf
Small detail.
Big difference.
Search aware truncation
Search results should not blindly cut text and hope the useful part survives.
If someone is searching inside logs, errors, docs, or long records, the preview should try to keep the match visible.
import { truncateAround } from "@tonybonet/truncate";
const result = truncateAround(logLine, {
font: "14px Geist Mono",
maxWidth: 320,
target: "Cannot read properties of undefined",
context: 18,
});
Because this:
[2026-06-03 14:22:10] production-api-worker...
is not as useful as this:
...TypeError: Cannot read properties of undefined...
That small difference makes search results, command palettes, logs, support tools, and debugging UIs feel much more intentional.
Range aware truncation
Sometimes you already know the exact part of the text that matters.
Maybe it came from search.
Maybe it came from a parser.
Maybe it came from your own indexing layer.
So you can preserve that range directly.
import { truncateRange } from "@tonybonet/truncate";
truncateRange(article, {
font: "16px Inter",
maxWidth: 320,
start: match.start,
end: match.end,
before: 12,
after: 12,
});
The API does not only return the text.
It also gives you metrics.
So if the range was preserved, you know.
If it could not fit, you know.
No fake confidence.
No silent weirdness.
Custom markers
You are not locked into ....
Sometimes you want something more explicit.
truncateByWidth(articleIntro, {
font: "16px Inter",
maxWidth: 260,
ellipsis: " READ MORE",
});
Sometimes you want something smaller.
truncateByWidth(mediaTitle, {
font: "16px Inter",
maxWidth: 220,
ellipsis: ".",
});
Again, tiny detail.
But UI is full of tiny details.
DOM free by default
Truncate does not depend on reading layout from the page.
At the core, it measures through Canvas2D.
That makes it useful for:
- browser apps
- OffscreenCanvas
- Node canvas polyfills
- tests
- server side utilities
- pre rendered snippets
Sometimes you do not want to wait for the DOM to decide what the text should be.
You want the string ready before it gets there.
Yes, it even supports GIFs
Because every tiny UI utility eventually meets the weird case.
Install
pnpm add @tonybonet/truncate
Or:
npm install @tonybonet/truncate
Links
Playground:
https://tonyblu331.github.io/truncate/
Docs:
https://github.com/tonyblu331/truncate?tab=readme-ov-file#api
Repo:
https://github.com/tonyblu331/truncate
If you have a weird truncation case, send it my way.
That is probably the best test for this.
Top comments (0)