DEV Community

Cover image for CSS ellipsis is great. Until the string itself matters.
Antonio Bonet
Antonio Bonet

Posted on

CSS ellipsis is great. Until the string itself matters.

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;
}
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

Because this:

really-long-file-name...
Enter fullscreen mode Exit fullscreen mode

is not always as useful as this:

really-long...v12.pdf
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

Because this:

[2026-06-03 14:22:10] production-api-worker...
Enter fullscreen mode Exit fullscreen mode

is not as useful as this:

...TypeError: Cannot read properties of undefined...
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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",
});
Enter fullscreen mode Exit fullscreen mode

Sometimes you want something smaller.

truncateByWidth(mediaTitle, {
  font: "16px Inter",
  maxWidth: 220,
  ellipsis: ".",
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Or:

npm install @tonybonet/truncate
Enter fullscreen mode Exit fullscreen mode

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)