Let CSS do the truncating (text-overflow: ellipsis for one line, line-clamp for several) and use ResizeObserver only to detect whether truncation is happening — comparing scrollWidth/scrollHeight to the client size after each resize — so you can add a tooltip, a "Show more" button or an accessible full label, or to compute a middle ellipsis that CSS cannot express.

Problem / Scenario Context

A file manager lists long file names in a resizable column. Designers want an ellipsis when names do not fit, a tooltip with the full name only when a name is actually truncated, and — for names like quarterly-report-2026-final-v3.pdf — a middle ellipsis (quarterly-rep…final-v3.pdf) so the extension stays visible. The first implementation adds title attributes to every row, so tooltips pop up on short names too, and a second one checks truncation on window.resize, missing every change caused by dragging the column divider.

Truncation depends on the element's width, not the window's. The Element Resize Detection Patterns topic covers why ResizeObserver is the right trigger; this page applies it to text.

Mechanics Explanation

With overflow: hidden and text-overflow: ellipsis (plus white-space: nowrap), the browser truncates visually but keeps the full text in the DOM. The element's clientWidth is the visible width; its scrollWidth is the width the full text would need. So:

truncated ⇔ scrollWidth > clientWidth (single line), or scrollHeight > clientHeight (clamped multi-line)

Both properties require layout. Inside a ResizeObserver callback, layout has just been computed, so reading them is cheap — as long as the reads come before any writes (see spotting forced reflow inside ResizeObserver callbacks).

Truncation state can change without the element resizing: text content changes, a web font loads, or a sibling's width changes inside a flex row. Observing the element covers resizes; content changes need a re-check when you set the text.

A middle ellipsis cannot be done in CSS. It needs a measurement: find the longest prefix and suffix that fit. Measuring each candidate string with the DOM would force many layouts; measuring with a canvas measureText using the element's computed font avoids layout entirely.

What CSS Does and What the Observer AddsTwo columns. CSS truncates visually with text-overflow ellipsis for one line and line-clamp for several lines, keeps the full text in the DOM, and needs no script. The ResizeObserver adds knowledge of whether truncation is happening, so tooltips and show-more buttons appear only when needed, and enables middle truncation that CSS cannot express.CSS alonetext-overflow: ellipsis for one lineline-clamp for several linesFull text stays in the DOMNo script, no measurementResizeObserver addsKnows whether text is actually truncatedTooltip or Show more only when neededMiddle ellipsis that keeps extensions visible

Comparison Table: Truncation Techniques

Need Technique Script? Knows it truncated?
One-line end ellipsis text-overflow: ellipsis no no
Multi-line clamp line-clamp / -webkit-line-clamp no no
Tooltip only when truncated CSS + RO check scrollWidth > clientWidth yes yes
"Show more" only when clamped CSS clamp + RO check scrollHeight > clientHeight yes yes
Middle ellipsis (file names) RO + canvas measureText + binary search yes yes
Fit text to width container query units (cqi) no n/a

Minimal Reproducible Example

TypeScript
// Tooltips on every row, and a check that only runs on window resize.
document.querySelectorAll<HTMLElement>('.filename').forEach((el) => (el.title = el.textContent!));
addEventListener('resize', () => {
  document.querySelectorAll<HTMLElement>('.filename').forEach((el) => {
    el.classList.toggle('is-truncated', el.scrollWidth > el.clientWidth);
  });
});

Drag the column divider: the window does not resize, so is-truncated never updates, and short names still show redundant tooltips.

Production-Safe Solution

TypeScript
// 1. Detection for CSS-truncated elements.
const truncObserver = new ResizeObserver((entries) => {
  // Read phase: all measurements first.
  const states = entries.map((e) => {
    const el = e.target as HTMLElement;
    const multi = el.classList.contains('clamp');
    return { el, truncated: multi ? el.scrollHeight > el.clientHeight + 1 : el.scrollWidth > el.clientWidth + 1 };
  });
  // Write phase.
  for (const { el, truncated } of states) {
    el.toggleAttribute('data-truncated', truncated);
    if (truncated) el.setAttribute('title', el.textContent ?? '');
    else el.removeAttribute('title');
  }
});

export function watchTruncation(el: HTMLElement): () => void {
  truncObserver.observe(el);
  return () => truncObserver.unobserve(el);
}

// 2. Middle ellipsis without layout thrash: measure with canvas.
const ctx = document.createElement('canvas').getContext('2d')!;

function middleEllipsis(full: string, maxPx: number, font: string, keepEnd = 12): string {
  ctx.font = font;
  if (ctx.measureText(full).width <= maxPx) return full;
  const end = full.slice(-keepEnd);
  let lo = 0, hi = full.length - keepEnd;
  while (lo < hi) {                                     // longest prefix that fits
    const mid = Math.ceil((lo + hi) / 2);
    const candidate = `${full.slice(0, mid)}${end}`;
    ctx.measureText(candidate).width <= maxPx ? (lo = mid) : (hi = mid - 1);
  }
  return `${full.slice(0, lo)}${end}`;
}

const middleObserver = new ResizeObserver((entries) => {
  for (const e of entries) {
    const el = e.target as HTMLElement;
    const full = el.dataset.full ?? (el.dataset.full = el.textContent ?? '');
    const width = e.contentBoxSize[0].inlineSize;
    const font = getComputedStyle(el).font;             // style read, layout already clean
    const text = middleEllipsis(full, width, font);
    if (el.textContent !== text) el.textContent = text; // text change does not resize a fixed-width cell
    el.setAttribute('aria-label', full);                // screen readers get the whole name
    el.toggleAttribute('data-truncated', text !== full);
  }
});

The detection observer reads everything before writing anything. The middle-ellipsis observer writes textContent, which is safe because the element's width is set by the column, not by its content; if its width depended on content (an inline element in a flex row sized to fit), the write would change the observed size and could loop — give such elements a definite width or min-width: 0 with flex: 1.

Middle-Truncating a File Name on ResizeFive steps. The column is resized and the observer delivers the cell's new content width. The full name is read from a data attribute. The canvas measures candidate strings using the cell's computed font, and a binary search finds the longest prefix that fits with the kept suffix. The cell's text is replaced only if it changed. The full name is exposed as the accessible label.1Resize deliveredNew inline size of the cell.2Full textFrom data-full, captured once.3Measure with canvasBinary search for the longest prefix plus kept suffix; no layout.4Write if changedtextContent only when the result differs.5Accessible namearia-label keeps the full file name for screen readers.

Accessibility of Truncated Text

Visual truncation hides information, and the title attribute is a weak remedy: it is not shown on touch devices and is inconsistently announced. Better options:

  • CSS truncation keeps the full text in the DOM, so screen readers read the whole thing already. No extra work is needed for them.
  • Middle ellipsis replaces the text, so the full value must be restored for assistive technology with aria-label (as above) on an element with an appropriate role, or with visually hidden full text alongside.
  • Tooltips should be keyboard-accessible: a focusable element that shows the full text on focus as well as hover, rather than title.
  • "Show more" buttons for clamped text must be real buttons with aria-expanded, shown only when data-truncated is set.

Choosing a Truncation ApproachA decision chain. If one-line end truncation is enough, use CSS text-overflow and add a ResizeObserver only if you need to know when it truncates. Otherwise, if multi-line clamping is needed, use line-clamp with a ResizeObserver to show a Show more button. Otherwise, if the end of the text matters, such as a file extension, compute a middle ellipsis with a ResizeObserver and canvas measurement. Otherwise consider scaling text with container query units.Is one-line end truncation enough?text-overflow: ellipsis; RO only to detect ityesnoSeveral lines, then clamp?line-clamp; RO shows Show more when clampedyesnoMust the end stay visible (extensions, IDs)?Middle ellipsis: RO + canvas measureTextyesnoConsider scaling the text with container query units instead.

Verification Steps

  • Drag column dividers and confirm truncation state and middle ellipses update continuously.
  • Hover short and long names and confirm tooltips appear only on truncated ones.
  • Use a screen reader and confirm full names are announced for middle-truncated cells.
  • Load a web font late and confirm truncation is re-evaluated (the font change resizes nothing; call the check after document.fonts.ready too).
  • Record a trace while resizing and confirm no forced layouts inside the callbacks.

Common Mistakes to Avoid

  • Checking truncation on window.resize. Container changes do not resize the window.
  • Adding title everywhere. Tooltips on untruncated text are noise.
  • Measuring candidates with DOM elements. Each measurement forces layout; use canvas.
  • Losing the full text for screen readers when replacing text with a middle ellipsis.

FAQ

How do I know if CSS text-overflow is actually truncating?

Compare scrollWidth with clientWidth on the element. If scrollWidth is larger, the text overflows and the ellipsis is showing. For line-clamp, compare scrollHeight with clientHeight.

Why add 1 pixel to the comparison?

Fractional widths are rounded differently for scrollWidth and clientWidth, which can make an exactly fitting text look one pixel too wide. The tolerance avoids flickering between truncated and not truncated.

Is canvas measureText accurate enough?

It uses the same font engine, so for a single font with no special features it matches closely. Kerning, ligatures and letter-spacing can create small differences; leave a few pixels of slack in the available width.

Does a web font loading trigger ResizeObserver?

Only if the element's size changes, which it usually does not for fixed-width cells. Re-run the truncation check after document.fonts.ready, or listen for loadingdone on document.fonts.

Can CSS do middle truncation?

Not directly. Some layouts approximate it with two flex items — a shrinking prefix with an ellipsis and a fixed suffix — which works when the split point is known, such as before a file extension.

Should every cell have its own observer?

No. One shared ResizeObserver for all cells delivers all resized cells in one callback, which is cheaper and lets you batch reads before writes across the whole column.


↑ Back to Element Resize Detection Patterns