299

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?

1
  • 1
    the ellipsis aspect is irrelevant; all you need to detect is whether it's overflowed. Commented Oct 12, 2011 at 10:58

21 Answers 21

468

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

function isEllipsisActive(e) {
     return (e.offsetWidth < e.scrollWidth);
}
Sign up to request clarification or add additional context in comments.

14 Comments

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.
For those who need to understand offsetWidth, scrollWidth, and clientWidth, here is a very good explanation: stackoverflow.com/a/21064102/1815779
On text-overflow:ellipsis it should be e.offsetWidth <= e.scrollWidth
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.
They're always equal to each other on flex children, and thus, won't work for them.
|
139

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.

25 Comments

@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!
Seems that this won't work in situations where there is no white-space: nowrap.
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.
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))...
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
|
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);
}

Comments

12

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!

2 Comments

It doesn't work on IE10 - offsetWidth is identical to scrollWidth, both giving me the truncated width.
I need this tolerance on Chrome 60 (latest) too.
9

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>

1 Comment

This implementation does work for cell in tables (ie. td element) whereas the top voted one doesn’t (I have the same value for offsetWidth and scrollWidth)
7

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

1 Comment

That's perfect! Exactly what I needed. Just couple of updates from 2023: it should be not $.expr[':'].truncated but $.expr.pseudos.truncated (as of jQuery 3.0), $(...).on('mouseenter', ...) and small improvement for single line $this.attr('title', isTruncated ? $this.text() : null);
6

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);
}

Comments

4

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'})
    }
  });
});

1 Comment

Neither works in current Chrome nor Firefox. Both elements become red.
4

By comparing the clientHeight to the scrollHeight it can be inferred that a truncation is happening if the scrollHeight is greater:

enter image description here

Resizable Example:

const isTextTruncated = node => node.scrollHeight > node.clientHeight
const setTextTruncatedClass = node => node.classList.toggle('truncated', isTextTruncated(node))

// Resize DOM observer
const resizeObserver = new ResizeObserver(m => setTextTruncatedClass(m[0].target))
resizeObserver.observe(document.querySelector('p'), { attributes: true })
p {
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  
  width: 300px;
  resize: horizontal;
  overflow: hidden;
}

/* detection label 
 ********************/
body:has(.truncated)::after {
  content: 'Truncated';
  font: 24px Arial;
  background: red;
  color: white;
  padding: 8px;
  position: absolute;
  bottom: 1em;
  right: 1em;
}
<p>
  <b>Resize this text block:</b> Lorem ipsum dolor sit amet, consectetur adipiscing elit, 
  sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 
  Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris 
  nisi ut aliquip ex ea commodo consequat.
</p>

👉 See my Codepen with a more configurable example

Comments

3

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>

Comments

3

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 Comment

This is highly risky since you are hard-coding elementItem.style.width to auto without caring if the element even had any other width style applied or inherited to it.
3

None of the approaches above was suitable for me, since offsetWidth, clientWidth, scrollWidth return integer values, and is not accurate enough to detect whether the ellipsis is active. Here is my approach:

const hasEllipsis = (element) => {
  // check if text actually overflows
  // use range-based check for better precision of content width
  const range = document.createRange();
  range.selectNodeContents(element);
  const rangeWidth = range.getBoundingClientRect().width;
  // use getBoundingClientRect for better precision
  const elementWidth = element.getBoundingClientRect().width;

  const isOverflowing = rangeWidth > elementWidth;

  return isOverflowing;
};

let el = document.querySelector('#test');
let el2 = document.querySelector('#test2');

document.querySelector('#result').innerHTML = 'First div is truncated? '+ (hasEllipsis(el) ? 'YES': 'NO') + '<br/>' + 'Second div is truncated? '+ (hasEllipsis(el2) ? 'YES': 'NO');
#test,#test2
{
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow:hidden;
 }

#test2
{
  max-width: 10rem;

}
<div id="test"> This is a very long sentence which should be truncated? </div>
<div id="test2"> This is a very long sentence which should be truncated? </div>

<p id="result"></p>

Comments

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

Comments

1

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
    }
  }}
/>

1 Comment

This would not work in today's React. You'd have to look at ref.current to get the element. Like, you'd need to adjust the above to ref.current.scrollWidth etc.
1

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

Comments

0

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 Comment

Loving the fact after 10 years this questions still gets action - although I suspect after 10 years browser standards have improved somewhat! :-)
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
    }

Comments

0

I was looking for determining the same so that I know whether or not to show a "Show more" button (for big instruction text that would then open in a modal). Anyway sharing a derived solution in React (w/ TypeScript). React as of 2025:

const InstructionBox = styled(Box)(() => ({
  display: '-webkit-box',
  WebkitBoxOrient: 'vertical',
  overflow: 'hidden',
  lineClamp: 3,
  WebkitLineClamp: 3
}));


  // and in the component code:
  const [showMore, setShowMore] = useState<boolean>(false);
  const instructionBoxTextRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (instructionBoxTextRef.current) {
      const { current } = instructionBoxTextRef;
      if (current.scrollHeight > current.offsetHeight) {
        setShowMore(true);
      }
    }
  }, [instructionBoxTextRef]);

  return (
  <>
    <InstructionBox ref={instructionBoxTextRef}>{instructionText}</InstructionBox>
    {showMore && <Button onClick={handleToggleOpen}>Show more</Button>}
  </>
);

3 Comments

Your solution is specific to React + Typescript, while repeating the same underlying principle from existing answers, therefore this is an unnecessary duplicate answer with even less value due to its irrelevant specificity to this general topic
Your attitude is ugly and snarky. I know exactly what my solution is. I mention it is derived. It is here because it is what I needed and found nowhere. Your answer was totally useless to me, but I didn't have to say anything nasty about it. I share in hopes of helping others. How do you have such an attitude with as high a rep? Your comment added less than no value. It is just ugly.
The point is - Stackoverflow would become a mess if everyone would post code solutions for their own use specific cases and their own framework usage for general question. Imagine every jquery, angular, vue etc. person posting an almost-identical answer, specifically for their usecase .The content quality will go down. I am caring a lot about content quality and relevancy here. A LOT. Stackoverflow is like a garden which needs endless maintenance, Regardless if this helps you, the correct thing to do is open a new question, specific for React and answer yourself there.
-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;
    }
}

2 Comments

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 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.
-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>

Comments

-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)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.