'Display as much elements as it can fit in one line, and if not all of them can fit add additional element that represents number of remaining elements

I would like to align all tags for a given article in one line.

If all the tags can fit in one line, then I want to display all of them, like this:

enter image description here

But, if there is more tags and not all of them can fit in the same line, I would like to display as much of them as it can fit, and I want to add additional label that represents number of remaining tags, like this:

enter image description here

I created this quick start code so everyone can jump and update what is necessary.

.card {
  padding: 5px;
  width: 300px;
  height: 300px;
  border: 2px solid blue;
  display: flex;
  flex-direction: row;
  gap: 5px;
}

.tag {
  padding: 5px 8px;
  color: #2C3C93;
  background: rgb(44, 60, 147, 0.1);
  height: 20px;
}
<div class="card">
   <div class="tag">Developing</div>
   <div class="tag">Busniess</div>
   <div class="tag">Web Design</div>
</div>


Solution 1:[1]

Try this

let cardWidth   = document.querySelector('.card').offsetWidth; // or 300
let tagsWidth   = 0;
let hiddenItems = 0;

document.querySelectorAll('.tag').forEach(tag => {
    tagsWidth += tag.offsetWidth;

    if( tagsWidth + 60 > cardWidth ){
        hiddenItems++;
        tag.style.display = 'none';
    }
})
if( hiddenItems > 0 ){
    document.querySelector('.card').insertAdjacentHTML('beforeend', `<div class="tag">${hiddenItems}+</div>`)
}
*,*::before,*::after {
  box-sizing: border-box;
}

.card {
  padding: 5px;
  width: 300px;
  height: 300px;
  border: 2px solid blue;
  display: flex;
  align-items: flex-start;
  gap: 5px;
}

.tag {
  padding: 5px 8px;
  color: #2C3C93;
  background: rgb(44, 60, 147, 0.1);
  white-space: nowrap;
}
<div class="card">
   <div class="tag">Developing</div>
   <div class="tag">Busniess</div>
   <div class="tag">Web Design</div>
</div>

Please note that the 60 here tagsWidth + 60 is the width of the "remaining/hidden tags div". This is to ensure that there is enough room for the "remaining/hidden tags div" if the tagsWidth exceeds the cardWidth.

Solution 2:[2]

Here is my attempt: https://jsfiddle.net/oneuj3yv/28/

const counterElementWidth = 30;
function collapseTags() {
  document.querySelectorAll('.card').forEach(card => {
    let counterTag = card.querySelector('.counter-tag');
  
    let cardStyle = getComputedStyle(card);
    let padding = parseFloat(cardStyle.paddingLeft) + parseFloat(cardStyle.paddingRight);
    let innerWidth = card.clientWidth - padding;
    
    let usedWidth = 0;
        let tagsLeft = 0;

        card.querySelectorAll('.tag').forEach(tag=>{
            usedWidth += tag.getBoundingClientRect().width;
            if(usedWidth >= innerWidth - counterElementWidth) {
        tag.classList.add('hidden');
        tagsLeft++;
      } else {
        tag.classList.remove('hidden');
      }
    });
    
    if(tagsLeft > 0) {
      counterTag.classList.remove('hidden');
      counterTag.innerHTML = `+${tagsLeft}`;
    } else {
        counterTag.classList.add('hidden');
    }
    
  });
}
collapseTags();
window.addEventListener('resize', collapseTags);

For the whole thing to work I have added some CSS and additional HTML to the fiddle as well.

It still seems a bit hit-or-miss on resize, but on load it works.

An improvement could be for the counter element to be dynamically generated.

Another future improvement could be the tweaking of counterElementWidth I used for the width of the counter (currently I just take a wild guess of 30 px).

Solution 3:[3]

You can use this:

const card = document.querySelector('.card');
const getWidth = (el) => {
 return el.getBoundingClientRect().width;
}
const widthCard = getWidth(card);
const tags = [...document.querySelectorAll('.tag')];
const totalWidth = tags.reduce((total, currentEl) => getWidth(currentEl) + total, 0);
if (totalWidth > widthCard) {
  const numberTag = document.createElement('div');
  numberTag.classList.add('tag');
  numberTag.textContent = tags.length + '+';
  card.appendChild(numberTag);
    const newElements = [];
  const remainingTags = [];
    let newTotal = getWidth(numberTag) + 19 // +19 for the padding and border and a gap;
  for (let tag of tags) {
    const width = getWidth(tag);
    if ((newTotal + width) < widthCard) {
        newElements.push(tag);
      newTotal += width + 5; // +5 for spacing
    } else {
        remainingTags.push(tag);
    }
  }
  
numberTag.textContent = `${tags.length - newElements.length}+`;
   newElements.push(numberTag);
  card.innerHTML = '';
  newElements.forEach((el) => {
    card.appendChild(el);
  })
  
  //You can use the remanining tags to show a tooltip, like this:
  numberTag.setAttribute('title', remainingTags.map(e => e.textContent).join(', '));
}

const card = document.querySelector('.card');
const getWidth = (el) => {
 return el.getBoundingClientRect().width;
}
const widthCard = getWidth(card);
const tags = [...document.querySelectorAll('.tag')];
const totalWidth = tags.reduce((total, currentEl) => getWidth(currentEl) + total, 0);
if (totalWidth > widthCard) {
  const numberTag = document.createElement('div');
  numberTag.classList.add('tag');
  numberTag.textContent = tags.length + '+';
  card.appendChild(numberTag);
    const newElements = [];
  const remainingTags = [];
    let newTotal = getWidth(numberTag) + 19 // +19 for the padding and border and a gap;
  for (let tag of tags) {
    const width = getWidth(tag);
    if ((newTotal + width) < widthCard) {
        newElements.push(tag);
      newTotal += width + 5; // +5 for spacing
    } else {
        remainingTags.push(tag);
    }
  }
  
numberTag.textContent = `${tags.length - newElements.length}+`;
   newElements.push(numberTag);
  card.innerHTML = '';
  newElements.forEach((el) => {
    card.appendChild(el);
  })
  
  //You can use the remanining tags to show a tooltip, like this:
  numberTag.setAttribute('title', remainingTags.map(e => e.textContent).join(', '));
}
*,*::before,*::after {
  box-sizing: border-box;
}

.card {
  padding: 5px;
  width: 300px;
  height: 300px;
  border: 2px solid blue;
  display: flex;
  flex-direction: row;
  gap: 5px;
}

.tag {
  padding: 5px 8px;
  color: #2C3C93;
  background: rgb(44, 60, 147, 0.1);
  height: fit-content;
  white-space: nowrap;
}
<div class="card">
   <div class="tag">Developing</div>
   <div class="tag">This is a very large tag</div>
   <div class="tag">Busniess1</div>
   <div class="tag">Web Design</div>
   <div class="tag">Web Design</div>
   <div class="tag">Web Design</div>
</div>

Also, you have to use a box-sizing of border-box for the elements.

You can see the fiddle here: https://jsfiddle.net/Tanay861/4ezkr2og/41/

Solution 4:[4]

You can create a custom element that does the following:

  1. Layout content elements as flex rows, by setting its height, it may be used to just show a single row or a max number of rows.
  2. Detect resize, and hide all overflowing content elements
  3. Position an info element when content elements do not fit

Example: https://jsfiddle.net/bkep1n2q/1/

(Try different values of max-height on <app-tags style="max-height: 80px;">, and resize the window)

JS:

export class Tags extends HTMLElement {

    #observer = null;

    #badge = document.createElement('span');

    connectedCallback(){
        this.#observer = new ResizeObserver(this.#update.bind(this));
        this.#observer.observe(this);
        this.#badge.classList.add('app-tags-badge');
        this.appendChild(this.#badge);
    }

    disconnectedCallback(){
        this.#observer?.disconnect();
    }

    #update(){
        const p = this.getBoundingClientRect();
        let hidden = 0;
        let lastVisible = null;
        this.childNodes.forEach(n => {
            if(n instanceof HTMLElement && n !== this.#badge){
                const c = n.getBoundingClientRect();
                if(p.left > c.left || p.right < c.right || p.top > c.top || p.bottom < c.bottom){
                    hidden++;
                    n.style.visibility = 'hidden';
                }else {
                    n.style.visibility = '';
                    lastVisible = n;
                }
            }
        });
        if(hidden < 1){
            this.#badge.style.display = 'none';
        }else if(lastVisible !== null){
            lastVisible.style.visibility = 'hidden';
            this.#badge.style.display = '';
            this.#badge.style.top = `${lastVisible.offsetTop}px`;
            this.#badge.style.left = `${lastVisible.offsetLeft}px`;
        }else {
            this.#badge.style.display = '';
            this.#badge.style.top = '0';
            this.#badge.style.left = '0';
        }

        this.#badge.innerHTML=`+${hidden} more`;
    }
}
window.customElements.define('app-tags',Tags);

CSS:

app-tags {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    overflow: hidden;
    gap: 12px;
    border: 1px solid red;
}

app-tags > * {
    min-width: 0;
}

app-tags > .app-tags-badge {
    position: absolute;
    padding: 6px;
}

.card {
    padding:6px;
    border: 1px solid #ccc;
    border-radius: 6px;
}

Usage:

<!DOCTYPE html>
<html>
    <body>
        <app-tags style="max-height: 80px;">
            <span class="card">Card 1</span>
            <span class="card">Card 2</span>
            <span class="card">Card 3</span>
            <span class="card">Card 4</span>
            <span class="card">Card 5</span>
            <span class="card">Card 6</span>
            <span class="card">Card 7</span>
            <span class="card">Card 8</span>
            <span class="card">Card 9</span>
            <span class="card">Card 10</span>
        </app-tags>
    </body>
</html>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Jelmertje
Solution 3
Solution 4