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), orscrollHeight > 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.
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
// 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
// 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.
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 whendata-truncatedis set.
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.readytoo). - 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
titleeverywhere. 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.
Related
- Building a Responsive Navigation Overflow Menu — overflow of whole items, not text
- Syncing Textarea Auto-Height with ResizeObserver — growing instead of truncating
- Detecting Container Queries with ResizeObserver — CSS-first alternatives
↑ Back to Element Resize Detection Patterns