ul
  li(data-js='detect-wrap') 
    a(href='https://dev.to/rossangus/detecting-how-many-times-some-text-wraps-with-javascript-1p35') Read the full writeup
  li(data-js='detect-wrap') Vivamus at felis quis est maximus porttitor eu eget lectus
  li(data-js='detect-wrap') Sed ipsum enim, euismod dictum justo non, placerat tincidunt risus. Duis feugiat in mi non pulvinar. Proin lorem enim, ornare sed rutrum in, facilisis eget sem. Donec vitae ex tincidunt, efficitur ex iaculis, gravida arcu
  li(data-js='detect-wrap') Pellentesque ultricies rhoncus arcu. Aliquam non ipsum neque. Pellentesque ante turpis, accumsan a feugiat id, euismod eget nisi. Integer lobortis, velit ut laoreet vestibulum, justo lorem varius dui, ut sagittis mauris ligula sed leo. Praesent eu metus dui. Praesent semper urna metus, nec consectetur quam efficitur vel
  li(data-js='detect-wrap') Aliquam erat volutpat. Quisque venenatis tellus vel varius laoreet. Phasellus accumsan faucibus enim, ut cursus metus viverra sit amet. Ut venenatis condimentum interdum. Curabitur et turpis at augue sollicitudin commodo in quis mi. Pellentesque fringilla blandit pellentesque. Morbi quis sem in neque elementum rutrum. Curabitur et urna et purus posuere ornare. Nullam tincidunt ut lacus non posuere. Vestibulum vulputate sit amet purus ac laoreet. Aenean ac arcu non eros gravida pharetra quis et sem. Donec vitae metus ut ipsum rutrum vulputate et et orci. In dictum vulputate consectetur. Quisque interdum finibus auctor. Ut congue nulla sagittis tellus sollicitudin, vel commodo augue rutrum. 
View Compiled
$boring-grey: rgb(128 128 128 / 0.5);

// Just some basic boilerplate. None of this CSS is required, for this to work
:root {
  --dark: #111;
  --light: #eee;
  --warning: rgb(255 0 0 / 0.2);
}

body {
  background: var(--light);
  color: var(--dark);
}

@media (prefers-color-scheme: dark) {
  body {
    background: var(--dark);
    color: var(--light);
  }
  
  a {
    color: #99f;
    
    &:visited {
      color: #f9f;
    }
  }
}

body {
  font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}

// Simple grid layout to force wrapping
ul {
  align-items: flex-start;
  display: flex;
  gap: 2em;
  padding: 0;
}

// Messing with the vertical box model to show that the maths works
li {
  border-bottom: solid 1em transparent;
  border-top: solid 1em transparent;
  flex: 1;
  list-style: none;
  margin: 1em 0;
  
  // Added just in case it influenced the box model height (it doesn't)
  outline: solid 0.2em transparent;
  padding: 3em 0 1em;
  position: relative;
  
  &::before,
  &::after {
    left: 50%;
    position: absolute;
    transform: translate(-50%, 0);
    z-index: 1;
  }
  
  // The wee bubble showing how many times the text wraps
  &::before {
    background: $boring-grey;
    
    // Can't think of an elegant way to stop the `s` appearing if the text
    // is just on one line
    content: 'Wrapping ' attr(data-wrap) ' times';
    font-size: 0.8em;
    padding: 0.5em;
    text-align: center;
    bottom: 100%;
  }  

  // The speechmark tick
  &::after {
    border: solid 1em transparent;
    border-top-color: $boring-grey;
    border-bottom-width: 0;
    content: "";
    bottom: calc(100% - 1em);
  }
}
View Compiled
// Pinched from here: https://amitd.co/code/typescript/debounce
const Debounce = (callback, timeout) => {
  let timer;

  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      callback(...args);
    }, timeout);
  };
};

// This is passed an element and returns an approximation of how many times
// the current text inside the element wraps
// Assumption: the element passed doesn't feature a mix of content, for example
// h1, ul, li, p. If this is done, it will return how many times plane text
// will wrap inside the element, rather than the original mix of block elements
const lineNumbers = (element) => {
  const elementStyles = window.getComputedStyle(element);

  // As clientHeight contained the padding top and bottom values, we need
  // to extract this information to remove it from the calculation
  const paddingTop = parseInt(elementStyles.getPropertyValue("padding-top"), 10);
  const paddingBottom = parseInt(elementStyles.getPropertyValue("padding-bottom"), 10);

  // Let's save this for later
  const currentContent = element.innerHTML;
  const contentHeight = element.clientHeight - paddingTop - paddingBottom;

  // Replace the content (temporarily!) with something which won't wrap
  element.innerHTML = "i";

  // Measure the height and store it
  const lineHeight = element.clientHeight - paddingTop - paddingBottom;

  // Put that content right back
  element.innerHTML = currentContent;
  
  // Approximation of how many times the line is wrapping
  return Math.round(contentHeight / lineHeight);
};

// Let's not stress the browser any more than is necessary
const checkForWrap = Debounce((elements) => {
  elements.forEach((element) => {
    element.setAttribute("data-wrap", lineNumbers(element));
  });
}, 100);

const DetectWrap = () => {
  window.addEventListener("resize", () => {
    // You could pass any selector you like
    checkForWrap(document.querySelectorAll('[data-js="detect-wrap"]'));
  });
  
  // Rather than calling checkForWrap() a second time, dispatch a resize event
  // to trigger it naturally
  window.dispatchEvent(new Event("resize"));
};

DetectWrap();
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.