<!-- 
pass to the ".truncate" element a "style" attribute 
with "--line-clamp" desired lines to clamp 
-->

<div class='truncate' contenteditable spellcheck='false'>
  The <em><code>-webkit-line-clamp</code></em> CSS property allows limiting of the contents of a block container to the specified number of lines.

  It <span>only</span> works in combination with the display property set to -webkit-box or -webkit-inline-box and the -webkit-box-orient property set to vertical.

  In most cases you will also want to set overflow to hidden, otherwise the contents won't be clipped but an <span>ellipsis</span> will still be shown after the specified number of lines.
</div>
.truncate{
  display: -webkit-box;
  -webkit-line-clamp: var(--line-clamp, 1);
  -webkit-box-orient: vertical;
  word-break: var(--word-break, 'none');
  overflow: hidden;
  hyphens: auto;
  text-align: var(--align, left);
  
  // Automatically use "word-break: break-all" for single-lines
  // (https://css-tricks.com/css-switch-case-conditions)
  --is-single-line: 1 - Clamp(0, Calc(var(--line-clamp) - 1), var(--line-clamp));
  --delay: Calc(-1s * (var(--is-single-line, 1) - 1)); 
  animation: states 1s var(--delay) paused; 
  
  @keyframes states {
      0% { word-break: break-all; }       
  }
}


/**** JUST FOR THIS DEMO: ****/

html,body{ height:100%; }
body{ 
  --bg: #4A90E2;
  background: var(--bg);
  height: 100vh;
  display: grid;
  place-items: center;
  font: calc(1vw + 1.2em)/1.4 Roboto, Arial;
  color: white;
}

.truncate{
  font-size: var(--font-size, 1em);
  width: var(--width, 50%);
  text-shadow: 1px 3px 0 rgba(black, .3);
  position: relative; 
  background: inherit;
  
  &:hover{
    outline: 2px dotted #FFFFFF99;
  }
  
  // fade text's last word, if clamped () 
  &::after{
    content: '';
    width: 0;
    height: 2ch;
    position: absolute;
    bottom: 0;
    right: 0;
    pointer-events: none;
    //background: linear-gradient(90deg, transparent, var(--bg));
    background: inherit; // it's easier to work with inheritance 
    mask-image: linear-gradient(to left, black, transparent);
    transition: .2s;
  }
  
  &.clamped{
    &::after{
      width: 3em;
    }
  }
  
  > span{
    text-decoration: underline;
  }
}
View Compiled
// Top-right controls 
const elm = document.querySelector('.truncate')
let debounce;

const myKnobs = new Knobs({
  CSSVarTarget: elm,
  visible: 2,
  knobs: [
    {
      cssVar: ['line-clamp'],
      label: 'Lines to clamp',
      type: 'range',
      value: 2,
      min: 1,
      max: 10,
      step: 1,
      onChange: checkTruncated
    },
    {
      cssVar: ['width', '%'],
      label: 'Width',
      type: 'range',
      value: 50,
      min: 10,
      max: 100,
      step: 1,
      onChange: checkTruncated
    },
    {
      cssVar: ['font-size', 'em'],
      label: 'Font size',
      type: 'range',
      value: 1,
      min: .6,
      max: 1.5,
      step: .1,
      onChange: checkTruncated
    },

    // break-all allows a better truncating for a single-line clamps,
    // so the text will fit best within the container
    /*
    {
      cssVar: ['word-break'],
      label: 'Word Break',
      type: 'checkbox',
      labelTitle: 'will automatically be "breal-all" for single-lines',
      checked: true,
      value: 'break-all'
    },
    */ 
    
    {
          cssVar: ['align'],
          label: 'Align text',
          type: 'radio',
          name: 'align-radio-group',
          options: [
            { value:'left', hidden:true, label: '<svg viewBox="0 0 28 28"><path d="M28 21v2c0 0.547-0.453 1-1 1h-26c-0.547 0-1-0.453-1-1v-2c0-0.547 0.453-1 1-1h26c0.547 0 1 0.453 1 1zM22 15v2c0 0.547-0.453 1-1 1h-20c-0.547 0-1-0.453-1-1v-2c0-0.547 0.453-1 1-1h20c0.547 0 1 0.453 1 1zM26 9v2c0 0.547-0.453 1-1 1h-24c-0.547 0-1-0.453-1-1v-2c0-0.547 0.453-1 1-1h24c0.547 0 1 0.453 1 1zM20 3v2c0 0.547-0.453 1-1 1h-18c-0.547 0-1-0.453-1-1v-2c0-0.547 0.453-1 1-1h18c0.547 0 1 0.453 1 1z"></path></svg>' },
            { value:'center', hidden:true, label:'<svg viewBox="0 0 1024 1024"><path d="M1024 768v73.143c0 20-16.571 36.571-36.571 36.571h-950.857c-20 0-36.571-16.571-36.571-36.571v-73.143c0-20 16.571-36.571 36.571-36.571h950.857c20 0 36.571 16.571 36.571 36.571zM804.571 548.571v73.143c0 20-16.571 36.571-36.571 36.571h-512c-20 0-36.571-16.571-36.571-36.571v-73.143c0-20 16.571-36.571 36.571-36.571h512c20 0 36.571 16.571 36.571 36.571zM950.857 329.143v73.143c0 20-16.571 36.571-36.571 36.571h-804.571c-20 0-36.571-16.571-36.571-36.571v-73.143c0-20 16.571-36.571 36.571-36.571h804.571c20 0 36.571 16.571 36.571 36.571zM731.429 109.714v73.143c0 20-16.571 36.571-36.571 36.571h-365.714c-20 0-36.571-16.571-36.571-36.571v-73.143c0-20 16.571-36.571 36.571-36.571h365.714c20 0 36.571 16.571 36.571 36.571z"></path></svg>' },
            { value:'right', hidden:true, label:'<svg viewBox="0 0 28 28"><path d="M28 21v2c0 0.547-0.453 1-1 1h-26c-0.547 0-1-0.453-1-1v-2c0-0.547 0.453-1 1-1h26c0.547 0 1 0.453 1 1zM28 15v2c0 0.547-0.453 1-1 1h-20c-0.547 0-1-0.453-1-1v-2c0-0.547 0.453-1 1-1h20c0.547 0 1 0.453 1 1zM28 9v2c0 0.547-0.453 1-1 1h-24c-0.547 0-1-0.453-1-1v-2c0-0.547 0.453-1 1-1h24c0.547 0 1 0.453 1 1zM28 3v2c0 0.547-0.453 1-1 1h-18c-0.547 0-1-0.453-1-1v-2c0-0.547 0.453-1 1-1h18c0.547 0 1 0.453 1 1z"></path></svg>' }
          ],
          value: 'left',
          defaultValue: 'left'
    },
    
    "Readonly:",
    {
      label: 'Truncated detected',
      type: 'checkbox',
      disabled: true,
      checked: true,
      id: 'isTruncatedToggle'
    },
  ]
})

const isTextClamped = elm => elm.scrollHeight > elm.clientHeight


function checkTruncated(){
  clearTimeout(debounce)
  debounce = setTimeout(() => {
    
    const clamped = isTextClamped(elm)
    myKnobs.DOM.form.querySelector('#isTruncatedToggle').checked = clamped
    elm.classList.toggle('clamped', clamped)
  }
  , 100)
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://unpkg.com/@yaireo/knobs@latest