229

I have a collection of block elements on a page. They all have the CSS rules white-space, overflow, text-overflow set so that overflowing text is trimmed and an ellipsis is used.

However, not all the elements overflow.

Is there anyway I can use javascript to detect which elements are overflowing?

Thanks.

Added: example HTML structure I am working with.

<td><span>Normal text</span></td>
<td><span>Long text that will be trimmed text</span></td>

The SPAN elements always fit in the cells, they have the ellipsis rule applied. I want to detect when the ellipsis is applied to the text content of the SPAN.

Italo Borssatto
  • 13,777
  • 7
  • 62
  • 83
deanoj
  • 2,691
  • 2
  • 16
  • 11
  • 14
    Not a duplicate! That question is on about one element within another, parent element. I am talking about text within a single element. In my case, the SPAN in the TD never overflow the TD, it's the text within the SPAN that overflows, and gets trimmed. That's what I am trying to detect! Sorry - I could have posed this question better I admit. – deanoj Oct 12 '11 at 10:13
  • Oh, I forgot to add - this only need to work on webkit if that helps... – deanoj Oct 12 '11 at 10:15
  • I did Ghommey just to see if it did work...it didn't. – deanoj Oct 12 '11 at 10:18
  • the ellipsis aspect is irrelevant; all you need to detect is whether it's overflowed. – Spudley Oct 12 '11 at 10:58

17 Answers17

368

Try this JS function, passing the span element as argument:

function isEllipsisActive(e) {
     return (e.offsetWidth < e.scrollWidth);
}
Italo Borssatto
  • 13,777
  • 7
  • 62
  • 83
  • 11
    This should be the answer - very elegant and works on Chrome and IE(9) – Roy Truelove Jun 25 '13 at 16:40
  • 18
    This answer and Alex's answer will not work in IE8; there are some odd cases where the scroll width and outerwidth are the same...but it has ellipsis, and then some cases where they are the same...and there is NO ellipsis. In other words, the first solution is the only one that works across all browsers. –  Dec 05 '13 at 16:01
  • 1
    This does not work for me. I get 0 all the time for every element, for both offsetWidth and scrollWidth, even though I can clearly see the ellipsis. (And I'm not using IE: I'm using the latest version of Chrome.) http://jsfiddle.net/brandonzylstra/hjk9mvcy/ – iconoclast Nov 04 '14 at 17:33
  • 5
    For those who need to understand offsetWidth, scrollWidth, and clientWidth, here is a very good explanation: http://stackoverflow.com/a/21064102/1815779 – Linh Dam Feb 04 '16 at 03:26
  • 10
    On `text-overflow:ellipsis` it should be `e.offsetWidth <= e.scrollWidth` – oriadam Feb 20 '17 at 08:39
  • 4
    I'm investigating an issue where the detection seems to be off by one character. I'm detecting overflow one character later than I should. I think there's a slight discrepancy where the ellipsis is shown when the text could actually otherwise fit. I tried temporarily disabling "text-overflow: ellipsis" and sure enough the text fit without any overflow. It might be a browser issue. I'm using Firefox 62 on WIndows 10. – Ken Lyon Sep 20 '18 at 22:32
  • 2
    I also tested with `clientWidth` instead of `offsetWidth` and the same issue was evident. – Ken Lyon Sep 20 '18 at 22:38
  • 14
    They're always equal to each other on flex children, and thus, won't work for them. – Kevin Beal Feb 20 '19 at 15:03
  • clean solution but as @user1026723 said it does not always work. Using Mac & Google Chrome there are cases in which offsetWidth equals scrollWidth, wrongly detecting a text-overflow. The 1st answer, although not as nice/simple, seems to work better. – carlesgg97 Jan 27 '21 at 10:58
  • 1
    Thanks this works on Chrome and FF ok in 2021. – bob May 12 '21 at 20:33
  • In my case `offsetWidth` and `scrollWidth` are always equal. Use `offsetHeight` and `scrollHeight` instead to detect overflow. See https://stackoverflow.com/questions/52232026/determine-if-ellipsis-is-being-displayed-when-using-webkit-line-clamp-for-multi – vxn Aug 27 '21 at 08:11
  • My offsetWidth is always 0 during elipsis. what is going one? – Nicolas S.Xu Sep 22 '21 at 20:03
131

Once upon a time I needed to do this, and the only cross-browser reliable solution I came across was hack job. I'm not the biggest fan of solutions like this, but it certainly produces the correct result time and time again.

The idea is that you clone the element, remove any bounding width, and test if the cloned element is wider than the original. If so, you know it's going to have been truncated.

For example, using jQuery:

var $element = $('#element-to-test');
var $c = $element
           .clone()
           .css({display: 'inline', width: 'auto', visibility: 'hidden'})
           .appendTo('body');

if( $c.width() > $element.width() ) {
    // text was truncated. 
    // do what you need to do
}

$c.remove();

I made a jsFiddle to demonstrate this, http://jsfiddle.net/cgzW8/2/

You could even create your own custom pseudo-selector for jQuery:

$.expr[':'].truncated = function(obj) {
  var $this = $(obj);
  var $c = $this
             .clone()
             .css({display: 'inline', width: 'auto', visibility: 'hidden'})
             .appendTo('body');

  var c_width = $c.width();
  $c.remove();

  if ( c_width > $this.width() )
    return true;
  else
    return false;
};

Then use it to find elements

$truncated_elements = $('.my-selector:truncated');

Demo: http://jsfiddle.net/cgzW8/293/

Hopefully this helps, hacky as it is.

Christian
  • 19,349
  • 3
  • 53
  • 70
  • thanks christian, i have seen this before and it looks like this is the only option available to me. – deanoj Oct 13 '11 at 13:20
  • Could this work with cloning the text, and seeing if texts match or not? – Lauri Apr 04 '13 at 14:33
  • 1
    @Lauri No; CSS truncation doesn't change the _actual_ text in the box, so the content is always the same, whether it's truncated, visible, or hidden. There is still no way to programatically get the truncated text, if there was then you wouldn't need to clone the element in the first place! – Christian Apr 05 '13 at 21:08
  • 1
    Seems that this won't work in situations where there is no `white-space: nowrap`. – Jakub Hampl Jul 23 '14 at 09:31
  • @JakubHampl What do you mean? The demo I provided right there in my answer has `white-space: nowrap` and it works... http://jsfiddle.net/cgzW8/2/ – Christian Jul 23 '14 at 14:07
  • @JakubHampl I just deleted `white-space: nowrap`, and it works in my demo. http://jsfiddle.net/cgzW8/291/. Tested in Chrome, Firefox, Safari, IE8, and IE11. – Christian Jul 23 '14 at 18:40
  • @JakubHampl Ok I see what you mean. Your wording confused me; it's not that it doesn't work, because it still works. It also works where it shouldn't, and that's the real problem. I'm not a mind reader, so when you tell someone their code "doesn't work", please explain why. It helps everyone involved. However, I've tried to come up with a solution to no avail, what you want to do seems quite difficult. – Christian Jul 24 '14 at 20:20
  • 2
    I must say that after searching a LOT on the internet and tried to implement many solutions, this by far the most reliable one that I found. This solution does not give different results between browsers like element.innerWidth or element.OffsetWidth does which have problem when using margins or padding.. Great solution, Well done. – Scription Sep 10 '14 at 14:06
  • I agree with Scription, this is the best solution I've found on the net regardless of hackiness. I did however find a problem: If the element in question is quite small, and if it resizes with the page, then it is possible for the difference between the cloned element's width and the original element's width to be less than a pixel. This means the ellipsis can appear even if your code says there is no difference in width. To fix this I used the answer from the following link for more accuracy: http://stackoverflow.com/questions/11907514/getting-the-actual-floating-point-width-of-an-element – Adam Goodwin May 12 '15 at 00:19
  • Just a note on my last comment: Comparing floating point widths resulted in the opposite problem, so I had to add a tolerance – i.e. if the difference between the cloned element's floating point width and the original's floating point width is less than 0.01 I consider them equal. I realise this makes this for an even more hacky solution but I don't see any other option if you need a more accurate width comparison. – Adam Goodwin May 12 '15 at 00:21
  • @AdamGoodwin Thanks for the tip. Subpixels are the bane of my existence. Can you create a jsFiddle with your updated code? I'll then take a look and merge it into my answer. – Christian May 12 '15 at 00:47
  • Here's your original jsFiddle modified with the change: http://jsfiddle.net/cgzW8/418/ The easiest way to see the difference is if you change the css to have a 171.5px width. In Chrome/Firefox/IE this causes an ellipsis to appear, but with the original JavaScript it is not recognised as being truncated, as the width is rounded to 172px. – Adam Goodwin May 12 '15 at 08:16
  • The getBoundingClientRect includes borders, so with the change I made it's actually giving 173.5 as the width. But the cloned object is being measured in the same way so it should be ok. There's more info on the function here: https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect – Adam Goodwin May 12 '15 at 08:31
  • Of the so many QA of elipsis detection, only this answer worked for me. The problem was boundry cases where elipsis was showing but still `scrollWidth` was less than `offsetWidth`, This one solved that. Made a small modification for my case though, instead of appending to `body` appended to current element(`td` in my case) and immediately removed it. Thanks a lot!! – Pawan Aug 12 '15 at 06:04
  • Great solution, just for me, 'inline-block' works instead of 'inline' – Codemole Sep 11 '15 at 02:08
  • 1
    For me, this did not work anywhere. I'm not sure, how it depends on CSS (i've tried to use it on input controls, the solutions below worked at least in Chrome and Firefox (but not in IE11))... – Alexander Nov 25 '15 at 10:09
  • 4
    Great solution, especially using jQuery pseudo-selector. But sometimes may not work, because width is calculated incorrectly if the element text has different style (font-size, letter-spacing, margins of inner elements). In that case i would recommend to append clone element to $this.parent() instead of 'body'. This will give exact copy of the element and calculations will be correct. Thanks anyway – Alex Feb 15 '16 at 14:08
  • This doesn't work for me in Firefox. When applying .width() on the truncated element I get 611px, but when getting the width of the newly added element I get 379px. And of course, the reason for this is that my original div had some CSS applied to it that doesn't apply anymore for the cloned div once taken out of its context. – SsjCosty Jan 23 '17 at 12:02
  • 1
    This isnt working for me. Im not using span, Im using input fields. Anyone know if there are any changes required to make this work for inputs? – discodowney Feb 17 '17 at 17:52
  • 1
    instead of appending it to body, maybe it's better to append it to ```element.parentNode```? (some css nesting rules that affect width may not apply when you append it to body) – Kevin Aug 21 '17 at 06:12
  • Here is a vanilla JS version: https://jsfiddle.net/Ld2pjabu/5/ Gist: https://gist.github.com/guacamoli/859d7853c137b905dc4dc950186d3a22 – Sahil Oct 25 '17 at 05:47
  • This is a good solution but as @Kevin mentioned, appending to $element.parent() will keep CSS rules intact that may otherwise make the clone wider than the original. Additionally, you may want to copy the class attribute from the element to the clone to avoid discrepancies caused by CSS. For example, the font size and font family applied by a class may make the clone wider than the original element. – clav Nov 29 '17 at 20:23
  • Hello @Christian Verga , i have applied same solution in the same problem but it won't work in a case of both width are same. It won't detect exactly. Please help me out if possible. – Unknown_Coder Apr 19 '18 at 01:48
  • This works but you need to take into account width of the ellipsis you can create span with `…` get the width of add to `$c.width()` or subtract from `$element.width()` – jcubic Sep 18 '18 at 15:09
  • The problem with appending it to the parentNode is, that in cases where whitespace is set to 'nowrap' or other restricting circumstances, the clone will wind up with the exact width of the element you're trying to check. The idea here is (I think) to append it in a place where the restrictions that caused the ellipsis to kick in do not apply. – Jürgen Simon Apr 27 '22 at 10:50
16

Adding to italo's answer, you can also do this using jQuery.

function isEllipsisActive($jQueryObject) {
    return ($jQueryObject.width() < $jQueryObject[0].scrollWidth);
}

Also, as Smoky pointed out, you may want to use jQuery outerWidth() instead of width().

function isEllipsisActive($jQueryObject) {
    return ($jQueryObject.outerWidth() < $jQueryObject[0].scrollWidth);
}
Alex K
  • 13,447
  • 4
  • 29
  • 30
15

For those using (or planning to use) the accepted answer from Christian Varga, please be aware of the performance issues.

Cloning/manipulating the DOM in such a way causes DOM Reflow (see an explanation on DOM reflow here) which is extremely resource intensive.

Using Christian Varga's solution on 100+ elements on a page caused a 4 second reflow delay during which the JS thread is locked. Considering JS is single-threaded this means a significant UX delay to the end user.

Italo Borssatto's answer should be the accepted one, it was approximately 10 times quicker during my profiling.

RyanGled
  • 386
  • 3
  • 9
  • 2
    Unfortunately it doesn't work in every scenario (I wish it would). And the performance hit that you mention here can be offset by only running it for situation in which you need to check - like only on `mouseenter` to see if a tooltip needs to be shown. – Jacob Jul 11 '19 at 18:44
7

Answer from italo is very good! However let me refine it a little:

function isEllipsisActive(e) {
   var tolerance = 2; // In px. Depends on the font you are using
   return e.offsetWidth + tolerance < e.scrollWidth;
}

Cross browser compatibility

If, in fact, you try the above code and use console.log to print out the values of e.offsetWidth and e.scrollWidth, you will notice, on IE, that, even when you have no text truncation, a value difference of 1px or 2px is experienced.

So, depending on the font size you use, allow a certain tolerance!

Andry
  • 15,262
  • 25
  • 130
  • 229
6

elem.offsetWdith VS ele.scrollWidth This work for me! https://jsfiddle.net/gustavojuan/210to9p1/

$(function() {
  $('.endtext').each(function(index, elem) {
    debugger;
    if(elem.offsetWidth !== elem.scrollWidth){
      $(this).css({color: '#FF0000'})
    }
  });
});
Matthew Herbst
  • 25,559
  • 22
  • 76
  • 116
Gustavo Juan
  • 61
  • 1
  • 1
4

This sample show tooltip on cell table with text truncated. Is dynamic based on table width:

$.expr[':'].truncated = function (obj) {
    var element = $(obj);

    return (element[0].scrollHeight > (element.innerHeight() + 1)) || (element[0].scrollWidth > (element.innerWidth() + 1));
};

$(document).ready(function () {
    $("td").mouseenter(function () {
        var cella = $(this);
        var isTruncated = cella.filter(":truncated").length > 0;
        if (isTruncated) 
            cella.attr("title", cella.text());
        else 
            cella.attr("title", null);
    });
});

Demo: https://jsfiddle.net/t4qs3tqs/

It works on all version of jQuery

Red
  • 53
  • 5
3

All the solutions did not really work for me, what did work was compare the elements scrollWidth to the scrollWidth of its parent (or child, depending on which element has the trigger).

When the child's scrollWidth is higher than its parents, it means .text-ellipsis is active.


When el is the parent element

function isEllipsisActive(el) {
    let width       = el.offsetWidth;
    let widthChild  = el.firstChild.offsetWidth;
    return (widthChild >= width);
}

When el is the child element

function isEllipsisActive(event) {
    let width       = el.offsetWidth;
    let widthParent = el.parentElement.scrollWidth;
    return (width >= widthParent);
}
Ruben Helsloot
  • 11,812
  • 5
  • 19
  • 40
Jeffrey Roosendaal
  • 6,430
  • 8
  • 36
  • 53
2

My implementation)

const items = Array.from(document.querySelectorAll('.item'));
items.forEach(item =>{
    item.style.color = checkEllipsis(item) ? 'red': 'black'
})

function checkEllipsis(el){
  const styles = getComputedStyle(el);
  const widthEl = parseFloat(styles.width);
  const ctx = document.createElement('canvas').getContext('2d');
  ctx.font = `${styles.fontSize} ${styles.fontFamily}`;
  const text = ctx.measureText(el.innerText);
  return text.width > widthEl;
}
.item{
  width: 60px;
  overflow: hidden;
  text-overflow: ellipsis;
}
      <div class="item">Short</div>
      <div class="item">Loooooooooooong</div>
Дмытрык
  • 243
  • 1
  • 12
1

I think the better way to detect it is use getClientRects(), it seems each rect has the same height, so we can caculate lines number with the number of different top value.

getClientRects work like this

function getRowRects(element) {
    var rects = [],
        clientRects = element.getClientRects(),
        len = clientRects.length,
        clientRect, top, rectsLen, rect, i;

    for(i=0; i<len; i++) {
        has = false;
        rectsLen = rects.length;
        clientRect = clientRects[i];
        top = clientRect.top;
        while(rectsLen--) {
            rect = rects[rectsLen];
            if (rect.top == top) {
                has = true;
                break;
            }
        }
        if(has) {
            rect.right = rect.right > clientRect.right ? rect.right : clientRect.right;
            rect.width = rect.right - rect.left;
        }
        else {
            rects.push({
                top: clientRect.top,
                right: clientRect.right,
                bottom: clientRect.bottom,
                left: clientRect.left,
                width: clientRect.width,
                height: clientRect.height
            });
        }
    }
    return rects;
}

getRowRects work like this

you can detect like this

Defims
  • 225
  • 1
  • 2
  • 9
1

None of the solutions worked for me, so I chose a totally different approach. Instead of using the CSS solution with ellipsis, I just cut the text from a specific string length.

  if (!this.isFullTextShown && this.text.length > 350) {
    return this.text.substring(0, 350) + '...'
  }
  return this.text

and show "more/less" buttons if the length is exceeded.

  <span
    v-if="text.length > 350"
    @click="isFullTextShown = !isFullTextShown"
  >
    {{ isFullTextShown ? 'show less' : 'show more' }}
  </span>
  • 1
    Loving the fact after 10 years this questions still gets action - although I suspect after 10 years browser standards have improved somewhat! :-) – deanoj Aug 04 '21 at 14:16
0

The e.offsetWidth < e.scrollWidth solution is not always working.

And if you want to use pure JavaScript, I recommend to use this:

(typescript)

public isEllipsisActive(element: HTMLElement): boolean {
    element.style.overflow = 'initial';
    const noEllipsisWidth = element.offsetWidth;
    element.style.overflow = 'hidden';
    const ellipsisWidth = element.offsetWidth;

    if (ellipsisWidth < noEllipsisWidth) {
      return true;
    } else {
      return false;
    }
}
David Guyon
  • 2,503
  • 1
  • 27
  • 39
  • 1
    Needing to cause a reflow for each element could potentially be pretty inefficient, like if you're using this across hundreds of cells in a table. – Ecksters Sep 24 '20 at 21:18
0

The solution @ItaloBorssatto is perfect. But before looking at SO - I made my decision. Here it is :)

const elems = document.querySelectorAll('span');
elems.forEach(elem => {
  checkEllipsis(elem);
});

function checkEllipsis(elem){
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  const styles = getComputedStyle(elem);
  ctx.font = `${styles.fontWeight} ${styles.fontSize} ${styles.fontFamily}`;
  const widthTxt = ctx.measureText(elem.innerText).width;
  if (widthTxt > parseFloat(styles.width)){
    elem.style.color = 'red'
  }
}
span.cat {
    display: block;
    border: 1px solid black;
    white-space: nowrap;
    width: 100px;
    overflow: hidden;
    text-overflow: ellipsis;
}
 <span class="cat">Small Cat</span>
      <span class="cat">Looooooooooooooong Cat</span>
Дмытрык
  • 243
  • 1
  • 12
0

there are some mistasks in demo http://jsfiddle.net/brandonzylstra/hjk9mvcy/ mentioned by https://stackoverflow.com/users/241142/iconoclast.

in his demo, add these code will works:

setTimeout(() => {      
  console.log(EntryElm[0].offsetWidth)
}, 0)
SONGJP
  • 166
  • 1
  • 7
0

If you're doing react, here's how I did it.

<div 
  ref={ref => {
    if (!ref) return
    const isOverflowing = ref.scrollWidth > ref.clientWidth
    if (isOverflowing) {
      // handle what to do next here
    }
  }}
/>
Alex Cory
  • 8,820
  • 7
  • 47
  • 60
0

Adding to @Дмытрык answer, missing deduction of borders and paddings to be fully functional!!

const items = Array.from(document.querySelectorAll('.item'));
items.forEach(item =>{
    item.style.color = checkEllipsis(item) ? 'red': 'black'
})

function checkEllipsis(el){
  const styles = getComputedStyle(el);
  const widthEl = parseFloat(styles.width);
  const ctx = document.createElement('canvas').getContext('2d');
  ctx.font = `${styles.fontSize} ${styles.fontFamily}`;
  const text = ctx.measureText(el.innerText);

  let extra = 0;
  extra += parseFloat(styles.getPropertyValue('border-left-width'));
  extra += parseFloat(styles.getPropertyValue('border-right-width'));
  extra += parseFloat(styles.getPropertyValue('padding-left'));
  extra += parseFloat(styles.getPropertyValue('padding-right'));
  return text.width > (widthEl - extra);
}
.item{
  width: 60px;
  overflow: hidden;
  text-overflow: ellipsis;
}
      <div class="item">Short</div>
      <div class="item">Loooooooooooong</div>
Mike
  • 680
  • 13
  • 39
0

For someone who uses e.offsetWidth < e.scrollWidth and got a bug that can show full text but still got ellipsis.

It because offsetWidth and scrollWidth always round the value. For example: offsetWidth return 161 but the actual width is 161.25. The solution is use getBoundingClientRect

const clonedEl = e.cloneNode(true)
clonedElement.style.overflow = "visible"
clonedElement.style.visibility = "hidden"
clonedElement.style.width = "fit-content"

e.parentElement.appendChild(clonedEl)
const fullWidth = clonedElement.getBoundingClientRect().width
const currentWidth = e.getBoundingClientRect().width

return currentWidth < fullWidth

Giang Le
  • 5,533
  • 3
  • 23
  • 24