
How to Truncate Text with Ellipsis in CSS (Single and Multi-Line)
Real content never matches the mockup. The product name that looked perfect in the design turns out to be forty characters longer in the database, a user's email address pushes a table column wide, and a card description wraps onto seven lines instead of three. Truncating text with an ellipsis (the "..." at the end) keeps the layout intact while signaling that there's more.
CSS handles this without JavaScript for both single-line and multi-line text. The single-line version is easy once you know the three required properties. The multi-line version relies on a prefixed property that's nonetheless supported everywhere. And both have a few well-known situations, especially inside flexbox and tables, where they silently refuse to work. This guide covers all of it.
Single-Line Truncation
To truncate text on one line, you need three properties working together:
<h3 class="card-title">
Ultra-Lightweight Waterproof Hiking Jacket with Packable Hood and Pit Zips
</h3>
.card-title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
Each property has a job:
white-space: nowrapstops the text from wrapping, so it overflows horizontally instead.overflow: hiddenclips the overflowing text at the element's edge.text-overflow: ellipsisreplaces the clipped part with an ellipsis character.
Leave out any one of them and the ellipsis won't appear. Without nowrap, the text just wraps. Without overflow: hidden, it spills out of the box. Without text-overflow, it's clipped abruptly mid-letter.
The Element Needs a Constrained Width
The ellipsis only appears when the text is wider than the element. A block element gets its width from its container, which is usually fine. An inline element, like a span, doesn't have a width you can constrain, so the technique won't work on it directly. Make it a block or inline-block and give it a width:
.username {
display: inline-block;
max-width: 12rem;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
vertical-align: bottom;
}
The vertical-align: bottom avoids a small gap below inline-block elements with overflow: hidden, which change their baseline alignment.
A Reusable Utility Class
Since you'll use this pattern often, keep it as a utility:
.truncate {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
Or as a Sass mixin:
@mixin truncate($max-width: 100%) {
max-width: $max-width;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.file-name {
@include truncate(20rem);
}
Tailwind users already have this as the truncate class.
Multi-Line Truncation with line-clamp
Truncating after several lines, for example a card description limited to three lines, uses -webkit-line-clamp:
<article class="post-card">
<h3 class="post-card__title">Getting Started with Container Queries</h3>
<p class="post-card__excerpt">
Container queries let components respond to the size of their parent rather
than the viewport. In this guide we build a card that switches layouts based
on available space, then look at container query units, style queries, and
how to combine them with existing media queries.
</p>
</article>
.post-card__excerpt {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
}
The text displays normally for up to three lines, and the third line ends in an ellipsis if there's more.
Yes, it uses display: -webkit-box, a legacy flexbox syntax, and two prefixed properties. That looks alarming, but this exact combination is supported in every major browser, including Firefox, because the specification standardized it for web compatibility. All four declarations are required; drop display: -webkit-box or -webkit-box-orient: vertical and the clamp won't apply.
What About the Unprefixed line-clamp?
The CSS Overflow specification defines a standard line-clamp property that doesn't need the legacy display value:
.post-card__excerpt {
line-clamp: 3;
}
Browser support for the unprefixed property is still limited and uneven as of this writing, so don't rely on it alone. If you want to be forward-compatible, include both, with the prefixed version doing the real work:
.post-card__excerpt {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
line-clamp: 3;
overflow: hidden;
}
Making the Line Count Configurable
A custom property lets you reuse one class with different line counts:
.line-clamp {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: var(--lines, 2);
overflow: hidden;
}
<p class="line-clamp" style="--lines: 4">...</p>
Reserving Height for Consistent Cards
Clamped text only takes as much height as it needs. A card with a one-line excerpt will be shorter than one with three clamped lines. If you want cards in a row to align, reserve the full height:
.post-card__excerpt {
--lines: 3;
line-height: 1.5;
min-height: calc(1lh * var(--lines));
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: var(--lines);
overflow: hidden;
}
The lh unit equals the element's computed line height. It's supported in current browsers; for older ones, use calc(1.5em * 3) with the same line-height value instead.
Watch Out for Padding
With overflow: hidden, any padding at the bottom of the clamped element can reveal part of the next line. Put padding on a wrapper instead of on the clamped element itself.
When Truncation Doesn't Work
These are the situations that account for most "why isn't my ellipsis showing?" questions.
Inside a Flex Container
This is the most common one. You have a flex row with an icon, a title, and a button, and the title refuses to truncate. Instead, it pushes the button out of the container.
<div class="list-item">
<img
class="list-item__avatar"
src="/avatars/amy.webp"
alt=""
width="40"
height="40"
/>
<span class="list-item__name"
>Amy Richardson-Montgomery (Senior Product Designer)</span
>
<button class="list-item__action">Message</button>
</div>
.list-item {
display: flex;
align-items: center;
gap: 0.75rem;
}
.list-item__name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
The cause is that flex items have min-width: auto by default, which means they won't shrink below the width of their content. With nowrap, the content is the entire unbroken line. Give the text item min-width: 0 and let it grow to fill the available space:
.list-item__name {
flex: 1;
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.list-item__avatar,
.list-item__action {
flex-shrink: 0;
}
If the truncating element is nested deeper, every flex item between it and the container may need min-width: 0 too. The same min-width: auto behavior applies to grid items; use min-width: 0 on the item, or minmax(0, 1fr) for the track.
.layout {
display: grid;
grid-template-columns: 16rem minmax(0, 1fr);
}
Inside a Table Cell
Table cells grow to fit their content by default, so text never overflows. Use a fixed table layout and give the column a width:
.data-table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.data-table .col-email {
width: 40%;
}
.data-table td {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
Alternatively, set a max-width on the cell. With automatic table layout, max-width: 0 combined with a percentage width is a known trick, but table-layout: fixed is more predictable.
No Width Constraint
If none of the ancestors limits the width, for example an absolutely positioned tooltip or an element inside a width: max-content container, there's nothing to overflow. Give the element or its container a max-width.
Beyond the Basic Ellipsis
Custom Overflow Strings
The spec allows a string value for text-overflow, like text-overflow: " [...]" or "-". Firefox supports it; other browsers generally don't. Browsers that don't support it ignore the declaration, so you can provide a fallback:
.teaser {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-overflow: " [more]";
}
Truncating at the Start
For content where the end matters more than the beginning, like file paths, you can clip from the left by reversing the direction:
.file-path {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
direction: rtl;
text-align: left;
}
This works well for plain paths, but direction: rtl can reorder punctuation at the ends of the string. Wrapping the text in a bdi element, or an inner span with direction: ltr and unicode-bidi: plaintext, helps in some cases. Test it with your real data.
Keeping the File Extension Visible
A common request is middle truncation: quarterly-report-final...v3.pdf. CSS can't truncate in the middle of a single string, but you can split the name into two elements and truncate only the first:
<span class="file-name">
<span class="file-name__base"
>quarterly-report-final-reviewed-by-legal-v3</span
>
<span class="file-name__ext">.pdf</span>
</span>
.file-name {
display: flex;
min-width: 0;
max-width: 18rem;
}
.file-name__base {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.file-name__ext {
flex-shrink: 0;
}
You'd do the split on the server or in your template.
Accessibility and Usability
Truncation hides information, so give users a way to get it back.
- Screen readers read the full text. The ellipsis is purely visual, so assistive technology still gets the complete content. That's good, but make sure the visual version isn't hiding something sighted users need to complete a task.
- Provide the full text on demand. Link truncated titles to the full item, use an expandable pattern like a
detailselement for long descriptions, or show the full value in a tooltip. - Use
titlewith care. Atitleattribute shows the full text on hover with a mouse, but it isn't available on touch devices or reliably to keyboard users. Treat it as a convenience, not the only way to see the content. - Don't truncate critical content. Error messages, prices, and legal text should wrap, not be clipped.
Show a Tooltip Only When Text Is Actually Truncated
A small script can add the title only when truncation occurs, so short text doesn't get redundant tooltips:
function addTitlesToTruncated(selector) {
document.querySelectorAll(selector).forEach((el) => {
const isTruncated =
el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight;
if (isTruncated) {
el.setAttribute("title", el.textContent.trim());
} else {
el.removeAttribute("title");
}
});
}
addTitlesToTruncated(".truncate, .line-clamp");
// Re-check when layout changes
const observer = new ResizeObserver(() =>
addTitlesToTruncated(".truncate, .line-clamp"),
);
document
.querySelectorAll(".truncate, .line-clamp")
.forEach((el) => observer.observe(el));
Quick Reference
/* Single line */
.truncate {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* Multiple lines */
.line-clamp {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: var(--lines, 2);
overflow: hidden;
}
/* Flex or grid child that should truncate */
.truncate-in-flex {
flex: 1;
min-width: 0;
}
Conclusion
Single-line truncation needs three properties, overflow: hidden, white-space: nowrap, and text-overflow: ellipsis, on an element with a constrained width. Multi-line truncation uses the -webkit-line-clamp combination, which despite its prefixes works in every major browser; add the unprefixed line-clamp for the future if you like.
When the ellipsis doesn't show up, the cause is almost always a missing width constraint: add min-width: 0 to flex and grid children, and use table-layout: fixed for tables. Pair truncation with a way to reveal the full text, and your layouts will stay tidy no matter what the content throws at them.


