Inline View More: A Better Text Truncation Pattern
We've all seen the classic "View More" button—sitting awkwardly below truncated text, taking up extra vertical space, and breaking the visual flow. There's a better way.
The Problem with Traditional Truncation
Most implementations look like this:
This is a long piece of text that gets cut off after
a certain number of lines and then you see...
[View More]
The button sits on its own line. It's clunky. It adds unnecessary height to your UI, especially problematic in data-dense interfaces like tables, approval workflows, or history logs.
The Inline Pattern
What if "View More" appeared inside the text, right where the ellipsis would be?
This is a long piece of text that gets cut off... View More
When expanded:
This is a long piece of text that gets cut off after a certain
number of lines. Now you can see the full content without any
awkward layout shifts. View Less
No extra rows. No layout jumps. Just seamless expansion in place.
Live Demo
<style>
.content {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.4;
word-wrap: break-word;
word-break: break-all;
position: relative;
transition: all 0.5s ease;
max-height: calc(1.4em * 3);
background: white;
white-space: normal !important;
}
.content.show-more::before {
content: " ... View more";
position: absolute;
right: 0;
bottom: 0;
background: #ffffff;
color: transparent;
z-index: 1;
}
.content.show-more::after {
content: " ... View more";
position: absolute;
right: 0;
bottom: 0;
z-index: 2;
background: linear-gradient(to right, black 0%, black 1em, blue 0.75em, blue 1em);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
cursor: pointer;
transition: opacity 0.5s ease;
white-space: pre;
}
.content.expanded {
-webkit-line-clamp: unset;
overflow: visible;
max-height: none;
}
.content.expanded::after {
content: " View less";
position: static;
right: 0;
bottom: 0;
background: white;
color: blue;
cursor: pointer;
transition: opacity 0.5s ease;
white-space: pre;
}
.content.show-more::after:hover,
.content.expanded::after:hover {
text-decoration: underline;
transform: scale(1.05);
}
</style>
<div>
<span class="content"
aria-expanded="false"
onclick="event.stopPropagation();
event.preventDefault();
Display.toggle(this)">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec gravida at libero rutrum porttitor. Nunc arcu diam, auctor in varius in, pharetra eget turpis. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Proin vitae ante in magna vehicula pretium. Sed vitae mauris orci. Aliquam venenatis mi ut augue commodo egestas. In lacus orci, accumsan id felis a, dignissim egestas erat. Sed pretium enim magna, non ultrices purus consequat vitae. Mauris vitae mauris vel orci iaculis interdum. Curabitur id velit ut arcu facilisis rhoncus a vitae urna. Curabitur nec risus quis nisl euismod molestie. Quisque dictum, orci eu congue fringilla, augue quam semper tortor, ac lacinia orci sem non nisi. Maecenas maximus felis eu augue pretium elementum. Sed diam nisl, sodales nec feugiat ac, maximus vel lectus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed vestibulum tortor arcu, vel tempor turpis imperdiet eu
</span>
</div>
<script>
const Display = {
resizeObserver: null,
toggle(element) {
if (
element.classList.contains("show-more") ||
element.classList.contains("expanded")
) {
if (element.classList.contains("expanded")) {
element.classList.remove("expanded");
element.setAttribute("aria-expanded", "false");
this.checkOverflow();
} else {
element.classList.add("expanded");
element.classList.remove("show-more");
element.setAttribute("aria-expanded", "true");
}
}
},
checkOverflow() {
const contents = document.querySelectorAll(".content");
contents.forEach((el) => {
if (!el.classList.contains("expanded")) {
const maxLines = 3;
el.style.webkitLineClamp = maxLines;
if (
el.scrollHeight > el.clientHeight ||
el.scrollWidth > el.clientWidth
) {
el.classList.add("show-more");
} else {
el.classList.remove("show-more");
}
}
});
},
cleanup() {
window.removeEventListener("resize", this._resizeHandler);
const toggles = document.querySelectorAll(".accordion-toggle");
toggles.forEach((toggle) => {
toggle.removeEventListener("click", this._toggleHandler);
});
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
},
init() {
this.cleanup();
// Bind handlers so we can remove them later
this._resizeHandler = () => this.checkOverflow();
this._toggleHandler = (e) => this.toggle(e.currentTarget);
setTimeout(() => this.checkOverflow(), 100);
window.addEventListener("resize", this._resizeHandler);
const pageContent = document.div;
if (pageContent && window.ResizeObserver) {
let resizeTimeout;
this.resizeObserver = new ResizeObserver(() => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => this.checkOverflow(), 10);
});
this.resizeObserver.observe(pageContent);
}
}
};
document.addEventListener("DOMContentLoaded", () => {
Display.init();
});
window.addEventListener("beforeunload", () => {
Display.cleanup();
});
</script>
How It Works
The trick is using CSS pseudo-elements (::before and ::after) to overlay the "View More" text at the exact truncation point and on top of it add few tweaks with Javascript.
Core CSS
.truncated-text {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
position: relative;
}
.truncated-text.show-more::after {
content: " ... View More";
position: absolute;
right: 0;
bottom: 0;
background: white;
color: var(--primary-color);
cursor: pointer;
}
.truncated-text.expanded {
-webkit-line-clamp: unset;
overflow: visible;
}
.truncated-text.expanded::after {
content: " View Less";
position: static;
}
The White Background Trick
The background: white on the pseudo-element covers the truncated text underneath. For extra polish, use a ::before as a mask layer:
.truncated-text.show-more::before {
content: " ... View More";
position: absolute;
right: 0;
bottom: 0;
background: white;
color: transparent; /* invisible mask */
z-index: 1;
}
.truncated-text.show-more::after {
/* same positioning, visible */
z-index: 2;
}
Detecting Overflow
Only add "View More" to text that actually overflows:
function checkOverflow(element) {
if (element.scrollHeight > element.clientHeight) {
element.classList.add('show-more');
} else {
element.classList.remove('show-more');
}
}
Toggle Logic
function toggle(element) {
const isExpanded = element.classList.toggle('expanded');
element.classList.toggle('show-more', !isExpanded);
element.setAttribute('aria-expanded', isExpanded);
}
Accessibility
<span class="truncated-text"
role="button"
tabindex="0"
aria-expanded="false"
onclick="toggle(this)"
onkeydown="if (event.key === 'Enter' || event.key === ' ') toggle(this)">
Your long text here...
</span>
Responsive Considerations
@media (max-width: 768px) {
.truncated-text {
-webkit-line-clamp: 2;
}
}
Re-check overflow on resize:
window.addEventListener('resize', () => {
document.querySelectorAll('.truncated-text').forEach(checkOverflow);
});
Gotchas
- Background color must match - Pseudo-element needs same background as container
-webkit-line-clamp support - Works in all modern browsers
- Dynamic content - Re-run overflow detection when content changes
- RTL languages - Position pseudo-elements on the left
Wrapping Up
Small UI details compound. An inline "View More" removes friction—one less layout jump, one less awkward button, one more polished interaction.
The best UI patterns are the ones users don't notice. They just work.