278

I have some elements on my page which have the CSS rules white-space, overflow, text-overflow set, so that overflowing text is trimmed and an ellipsis is used.

div {
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;

  border: 1px solid black;
  width: 150px;
  margin: 5px;
  padding: 5px;
  font-family: sans-serif;
}
<div>
  <span>Normal text</span>
</div>
<div>
  <span>Long text that will be trimmed text</span>
</div>

Is there any way I can use JavaScript to detect which elements' contents are overflowing?

miken32
  • 42,008
  • 16
  • 111
  • 154
deanoj
  • 3,191
  • 2
  • 17
  • 11

18 Answers18

429

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

function isEllipsisActive(e) {
     return (e.offsetWidth < e.scrollWidth);
}
Italo Borssatto
  • 15,044
  • 7
  • 62
  • 88
  • 19
    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
  • 6
    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
  • 12
    On `text-overflow:ellipsis` it should be `e.offsetWidth <= e.scrollWidth` – oriadam Feb 20 '17 at 08:39
  • 5
    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
  • 18
    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
  • 1
    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
  • 1
    My offsetWidth is always 0 during elipsis. what is going one? – Nicolas S.Xu Sep 22 '21 at 20:03
  • @NicolasS.Xu I believe if the element is a `span`, you might get zeros in these properties. So one solution is to wrap the `span` in a `div` which has `overflow: hidden` and then check `div` properties instead. – vgru Oct 04 '22 at 12:30
  • 1
    @KenLyon you can get more precise values by using `getBoundingClientRect.width`. Check out this solution: stackoverflow.com/a/69245632/14302216 – Leandro Ortiz Apr 20 '23 at 15:27
  • @LeandroOrtiz Perfect, thanks! That's exactly the scenario I observed. Now I just need remember what I needed this for 5 years ago. :) – Ken Lyon Apr 21 '23 at 17:38
  • clientWidth / clientHeight is better. offset will work with normal overflow but will not with line-clamp option. – Rafał Myszyński Aug 13 '23 at 10:07
136

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,605
  • 3
  • 54
  • 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
  • 6
    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. – Costin_T 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
17

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
  • 14,893
  • 4
  • 31
  • 32
9

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
  • 16,172
  • 27
  • 138
  • 246
6

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
  • 73
  • 1
  • 5
5

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
  • 29,477
  • 23
  • 85
  • 128
Gustavo Juan
  • 67
  • 1
  • 1
4

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
  • 12,582
  • 6
  • 26
  • 49
Jeffrey Roosendaal
  • 6,872
  • 8
  • 37
  • 55
4

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>
Dmytro
  • 304
  • 2
  • 13
2

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
  • 10,635
  • 10
  • 52
  • 62
2

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
  • 741
  • 13
  • 40
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
  • 233
  • 1
  • 2
  • 11
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

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
  • 6,446
  • 4
  • 23
  • 26
0

Case you are using line-clamp >= 2 line for adding ellipsis at more than one line you can use this conditioning:

if (
      descriptionElement &&
      descriptionElement.offsetHeight < descriptionElement.scrollHeight
    ) {
      // has text-overflow
    }
Alessander França
  • 2,697
  • 2
  • 29
  • 52
0

There's a small pixel problem with the answers above when comparing offsetWidth > scrollWidth.

W3C has a legacy API that returns element.scrollWidth value as rounded which is causing the comparison in some cases to to return false. If the element width are 150px and the scrollWidth are 150.4px (rounded to 150), then this check will be returning false even if the browser are showing ellipsis in the text.

They have tried to update the APIs that return fractional pixels, but it was reverted due to webcompat.

There's a workaround using max-content and getClientRects(). Here's a sample code that I use onMouseEnter. Note that this only works if the container has a boundary to 100% of the available width (so if you are using flexbox, your container has to be flex: 1 for example.

hasEllipsis(elementItem) {
    let scrollWidth = elementItem.scrollWidth;
    
    elementItem.style.width = 'max-content';
    const itemRects = elementItem.getClientRects();

    if (itemRects.length > 0 && itemRects[0].width > scrollWidth) {
        scrollWidth = itemRects[0].width;
    }

    elementItem.style.width = 'auto';
    return scrollWidth > elementItem.clientWidth;
}

Articles:

https://bugs.chromium.org/p/chromium/issues/detail?id=980476

https://github.com/w3c/csswg-drafts/issues/4123

-1

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,759
  • 1
  • 28
  • 40
  • 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
  • @Ecksters Moreover, a *query function* that returns the current state of an element should **never be allowed to change any property** of that element, especially the property queried. – Rene van der Lende Aug 30 '23 at 12:01
-1

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>
Dmytro
  • 304
  • 2
  • 13
-1

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
  • 334
  • 3
  • 8