Benito Serna Tips and tools for Ruby on Rails developers

A "read-more" behavior truncating by number of lines with css line-clamp and Stimulus.js

April 3, 2021, updated on January 24, 2024

I have already shared a way of implementing a “read-more” behavior truncating by the number of lines instead of the number of words.

But now I want to share how you can do it using the line-clamp css property.

The example

Visit example page

The html and css

Your are going to need…

html { line-height: 1.5 }
.hide { display: none; }
.line-clamp {
  overflow : hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-box-orient: vertical;
}
<div data-controller="read-more"
     data-read-more-lines-value="3"
     data-read-more-hide-class="hide"
     data-read-more-truncate-class="line-clamp"
     data-action="resize@window->read-more#render">
  <p data-read-more-target="content">
    The content that you want to truncate...
  </p>
  <button class="hide"
    data-read-more-target="moreButton"
    data-action="read-more#showMore">
    Show more
  </button>
  <button class="hide"
    data-read-more-target="lessButton"
    data-action="read-more#showLess">
    Show less
  </button>
</div>

The javascript

You are going to use line-clamp to truncate the content, assigning the configured linesValue to the property -webkit-line-clamp.

But you are going to do it just if the height is greater that the expectedHeigt that is the product of the linesValue with the lineHeight.

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["content", "moreButton", "lessButton"];
  static classes = ["truncate", "hide"]
  static values = { lines: Number }

  connect() {
    this.render()
  }

  render() {
    this.showAllContent()

    if (this.height() > this.expectedHeight()) {
      this.showLess()
    } else {
      this.showAllContent()
      this.hide(this.moreButtonTarget);
      this.hide(this.lessButtonTarget);
    }
  }

  showMore() {
    this.showAllContent();
    this.hide(this.moreButtonTarget);
    this.show(this.lessButtonTarget);
  }

  showLess() {
    this.truncateContent();
    this.hide(this.lessButtonTarget);
    this.show(this.moreButtonTarget);
  }

  showAllContent() {
    this.contentTarget.classList.remove(this.truncateClass);
  }

  truncateContent() {
    this.contentTarget.style["-webkit-line-clamp"] = this.linesValue;
    this.contentTarget.classList.add(this.truncateClass);
  }

  show(target) {
    target.classList.remove(this.hideClass)
  }

  hide(target) {
    target.classList.add(this.hideClass)
  }

  lineHeight() {
    let style = window.getComputedStyle(this.contentTarget)
    return parseFloat(style.lineHeight, 10);
  }

  height() {
    return this.contentTarget.offsetHeight;
  }

  expectedHeight() {
    return this.linesValue * this.lineHeight();
  }
}

Other ways to do it…

If for some reason, you can’t use line-clamp, maybe you can try doing the truncation with custom javascript.

And if you can truncate by number of characters, you can check this example using the “truncate” helper.

Subscribe to get future posts via email

Here I try to share knowledge and fixes to common problems and struggles for ruby on rails developers, like How to fetch the latest-N-of-each record or How to test that an specific mail was sent or a Capybara cheatsheet. You can see more examples on Most recent posts or all posts.