CSS for line clamp ellipsis

GitHub

Fixed number of lines

Here's the CSS required to make a paragraph truncate with an ellipsis after a specific number of lines (the vendor prefixes are required):

.line-clamp {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 5;
  overflow: hidden;
}

Here is the above CSS in action. Try adjusting the paragraph's width:

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Dolor sit amet consectetur adipiscing elit duis tristique sollicitudin. Suscipit adipiscing bibendum est ultricies integer quis auctor. Odio facilisis mauris sit amet massa vitae tortor condimentum lacinia. Nam libero justo laoreet sit amet cursus sit amet. Nisl vel pretium lectus quam id. Proin nibh nisl condimentum id. Sagittis vitae et leo duis ut diam quam nulla. Sed cras ornare arcu dui vivamus arcu felis. Vulputate ut pharetra sit amet aliquam id diam. Id neque aliquam vestibulum morbi. Tellus at urna condimentum mattis. Non enim praesent elementum facilisis leo vel. Turpis massa tincidunt dui ut. Risus quis varius quam quisque id. Amet purus gravida quis blandit. Felis eget velit aliquet sagittis id consectetur purus ut. Quis commodo odio aenean sed adipiscing. Pellentesque elit eget gravida cum sociis natoque penatibus et magnis. Facilisis leo vel fringilla est ullamcorper eget nulla. Montes nascetur ridiculus mus mauris. Nunc sed augue lacus viverra vitae congue. Morbi non arcu risus quis varius quam quisque id. Adipiscing tristique risus nec feugiat in fermentum posuere urna nec. Elit ullamcorper dignissim cras tincidunt lobortis feugiat vivamus.

Dynamic number of lines

You can also use a CSS variable and some JavaScript to dynamically set the -webkit-line-clamp property based on the container's height. Using this CSS:

.dynamic-line-clamp {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: var(--lines);
  overflow: hidden;
}

And this JavaScript:

function getLineHeightInPx(elem) {
  const temp = document.createElement('div');
  temp.textContent = '0';
  elem.insertAdjacentElement('beforeend', temp);
  const h = temp.scrollHeight;
  temp.remove();
  return h;
}

const elem = document.querySelector('.dynamic-line-clamp');
new ResizeObserver(_ => {
  elem.style.setProperty('--lines', Math.floor(elem.clientHeight / getLineHeightInPx(elem)));
}).observe(elem);

Try adjusting the height:

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Dolor sit amet consectetur adipiscing elit duis tristique sollicitudin. Suscipit adipiscing bibendum est ultricies integer quis auctor. Odio facilisis mauris sit amet massa vitae tortor condimentum lacinia. Nam libero justo laoreet sit amet cursus sit amet. Nisl vel pretium lectus quam id. Proin nibh nisl condimentum id. Sagittis vitae et leo duis ut diam quam nulla. Sed cras ornare arcu dui vivamus arcu felis. Vulputate ut pharetra sit amet aliquam id diam. Id neque aliquam vestibulum morbi. Tellus at urna condimentum mattis. Non enim praesent elementum facilisis leo vel. Turpis massa tincidunt dui ut. Risus quis varius quam quisque id. Amet purus gravida quis blandit. Felis eget velit aliquet sagittis id consectetur purus ut. Quis commodo odio aenean sed adipiscing. Pellentesque elit eget gravida cum sociis natoque penatibus et magnis. Facilisis leo vel fringilla est ullamcorper eget nulla. Montes nascetur ridiculus mus mauris. Nunc sed augue lacus viverra vitae congue. Morbi non arcu risus quis varius quam quisque id. Adipiscing tristique risus nec feugiat in fermentum posuere urna nec. Elit ullamcorper dignissim cras tincidunt lobortis feugiat vivamus.

Note that overflow: hidden will only hide text that is outside the container element. So if you resize the paragraph to cut through the middle of a line, the ellipsis will appear on the line above it but the cut-off text will still be visible.

Dynamic number of lines: quality of life improvements

If your paragraph's height is dynamic, it's useful to add some JavaScript to snap the paragraph's height to a multiple of its line-height whenever the paragraph is resized. To do so, update the event listener:

new ResizeObserver(_ => {
  const lineHeight = getLineHeightInPx(elem);
  const lines = Math.floor(elem.clientHeight / lineHeight);
  elem.style.setProperty('--lines', lines);
  elem.style.setProperty('height', `${lines * lineHeight}px`);
}).observe(elem);

It's also best to debounce the event listener for better performance and to prevent the event listener from fighting with the user for control over the element's height while the user is resizing it. Using this basic debounce function:

function debounce(fn, wait) {
  let timeoutId;
  return () => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(fn, wait);
  }
}

You can update the event listener to cancel if another event is fired within 200 milliseconds:

new ResizeObserver(debounce(() => {
  const lineHeight = getLineHeightInPx(elem);
  const lines = Math.floor(elem.clientHeight / lineHeight);
  elem.style.setProperty('--lines', lines);
  elem.style.setProperty('height', `${lines * lineHeight}px`);
}, 200)).observe(elem);

Full example

Here's the final CSS:

.dynamic-line-clamp {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: var(--lines);
  overflow: hidden;
}

And the final combined JavaScript with all the bells and whistles:

function debounce(fn, wait) {
  let timeoutId;
  return () => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(fn, wait);
  }
}

function getLineHeightInPx(elem) {
  const temp = document.createElement('div');
  temp.textContent = '0';
  elem.insertAdjacentElement('beforeend', temp);
  const h = temp.scrollHeight;
  temp.remove();
  return h;
}

const elem = document.querySelector('.dynamic-line-clamp');
new ResizeObserver(debounce(() => {
  const lineHeight = getLineHeightInPx(elem);
  const lines = Math.floor(elem.clientHeight / lineHeight);
  elem.style.setProperty('--lines', lines);
  elem.style.setProperty('height', `${lines * lineHeight}px`);
}, 200)).observe(elem);

And here it is in action!

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Dolor sit amet consectetur adipiscing elit duis tristique sollicitudin. Suscipit adipiscing bibendum est ultricies integer quis auctor. Odio facilisis mauris sit amet massa vitae tortor condimentum lacinia. Nam libero justo laoreet sit amet cursus sit amet. Nisl vel pretium lectus quam id. Proin nibh nisl condimentum id. Sagittis vitae et leo duis ut diam quam nulla. Sed cras ornare arcu dui vivamus arcu felis. Vulputate ut pharetra sit amet aliquam id diam. Id neque aliquam vestibulum morbi. Tellus at urna condimentum mattis. Non enim praesent elementum facilisis leo vel. Turpis massa tincidunt dui ut. Risus quis varius quam quisque id. Amet purus gravida quis blandit. Felis eget velit aliquet sagittis id consectetur purus ut. Quis commodo odio aenean sed adipiscing. Pellentesque elit eget gravida cum sociis natoque penatibus et magnis. Facilisis leo vel fringilla est ullamcorper eget nulla. Montes nascetur ridiculus mus mauris. Nunc sed augue lacus viverra vitae congue. Morbi non arcu risus quis varius quam quisque id. Adipiscing tristique risus nec feugiat in fermentum posuere urna nec. Elit ullamcorper dignissim cras tincidunt lobortis feugiat vivamus.