How to detect CSS flex wrap event

JavascriptJqueryCssFlexbox

Javascript Problem Overview


I have flex container with items inside. How to detect flex wrap event? I want to apply some new css to elements that have been wrapped. I suppose that it is impossible to detect wrap event by pure css. But it would be very powerful feature! I can try to "catch" this break point event by media query when element wraps into new line/row. But this is a terrible approach. I can try to detect it by script, but it's also not very good.

Look at the picture

I am very surprised, but simple $("#element").resize() doesn't work to detect height or width changes of flex container to apply appropriate css to child elements. LOL.

I have found that only this example of jquery code works https://stackoverflow.com/questions/18743144/jquery-event-listen-on-position-changed

But still terribly.

Javascript Solutions


Solution 1 - Javascript

Here's one potential solution. There might be other gotchas and edge cases you need to check for.

The basic idea is to loop through the flex items and test their top position against the previous sibling. If the top value is greater (hence further down the page) then the item has wrapped.

The function detectWrap returns an array of DOM elements that have wrapped, and could be used to style as desired.

The function could ideally be used with a ResizeObserver (while using window's resize event as a fallback) as a trigger to check for wrapping as the window is resized or as elements in the page change due to scripts and other user-interaction. Because the StackOverflow code window doesn't resize it won't work here.

Here's a CodePen that works with a screen resize.

var detectWrap = function(className) {
  
  var wrappedItems = [];
  var prevItem = {};
  var currItem = {};
  var items = document.getElementsByClassName(className);

  for (var i = 0; i < items.length; i++) {
    currItem = items[i].getBoundingClientRect();
    if (prevItem && prevItem.top < currItem.top) {
      wrappedItems.push(items[i]);
    }
    prevItem = currItem;
  };
  
  return wrappedItems;

}

window.onload = function(event){
  var wrappedItems = detectWrap('item');
  for (var k = 0; k < wrappedItems.length; k++) {
    wrappedItems[k].className = "wrapped";
  }
};

div  {
  display: flex;
  flex-wrap: wrap;
}

div > div {
  flex-grow: 1;
  flex-shrink: 1;
  justify-content: center;
  background-color: #222222;
  padding: 20px 0px;
  color: #FFFFFF;
  font-family: Arial;
  min-width: 300px;
}

div.wrapped {
  background-color: red;
}

<div>
  <div class="item">A</div>
  <div class="item">B</div>
  <div class="item">C</div>
</div>

Solution 2 - Javascript

Little bit improved snippet on jQuery for this purpose.

wrapped();

$(window).resize(function() {
   wrapped();
});

function wrapped() {
    var offset_top_prev;

    $('.flex-item').each(function() {
       var offset_top = $(this).offset().top;

      if (offset_top > offset_top_prev) {
         $(this).addClass('wrapped');
      } else if (offset_top == offset_top_prev) {
         $(this).removeClass('wrapped');
      }

      offset_top_prev = offset_top;
   });
}

Solution 3 - Javascript

I've modified sansSpoon's code to work even if the element isn't at the absolute top of the page. Codepen: https://codepen.io/tropix126/pen/poEwpVd

function detectWrap(node) {
    for (const container of node) {
        for (const child of container.children) {
            if (child.offsetTop > container.offsetTop) {
                child.classList.add("wrapped");
            } else {
                child.classList.remove("wrapped");
            }
        }
    }
}

Note that margin-top shouldn't be applied to items since it's factored into getBoundingClientRect and will trigger the wrapped class to apply on all items.

Solution 4 - Javascript

I'm using a similar approach in determining if a <li> has been wrapped in an <ul> that has it's display set to flex.

ul = document.querySelectorAll('.list');

function wrapped(ul) {

	// loops over all found lists on the page - HTML Collection
	for (var i=0; i<ul.length; i++) {

		//Children gets all the list items as another HTML Collection
		li = ul[i].children;

		for (var j=0; j<li.length; j++) {
			// offsetTop will get the vertical distance of the li from the ul.
			// if > 0 it has been wrapped.
			loc = li[j].offsetTop;
			if (loc > 0) {
				li[j].className = 'wrapped';
			} else {
				li[j].className = 'unwrapped';
			}
		}
	}
}

Solution 5 - Javascript

I noticed elements will typically wrap in relation to the first element. Comparing offset top of each element to the first element is a simpler approach. This works for wrap and wrap-reverse. (Probably won't work if elements use flex order)

var wrappers = $('.flex[class*="flex-wrap"]'); //select flex wrap and wrap-reverse elements

    if (wrappers.length) { //don't add listener if no flex elements
        $(window)
            .on('resize', function() {
                wrappers.each(function() {
                    var prnt = $(this),
                        chldrn = prnt.children(':not(:first-child)'), //select flex items
                        frst = prnt.children().first();

                    chldrn.each(function(i, e) { $(e).toggleClass('flex-wrapped', $(e).offset().top != frst.offset().top); }); //element has wrapped
                    prnt.toggleClass('flex-wrapping', !!prnt.find('.flex-wrapped').length); //wrapping has started
                    frst.toggleClass('flex-wrapped', !!!chldrn.filter(':not(.flex-wrapped)').length); //all are wrapped
               });
            })
            .trigger('resize'); //lazy way to initially call the above
    }

.flex {
    display: flex;
}

.flex.flex-wrap {
    flex-wrap: wrap;
}

.flex.flex-wrap-reverse {
    flex-wrap: wrap-reverse;
}

.flex.flex-1 > * { /*make items equal width*/
    flex: 1;
}

.flex > * {
  flex-grow: 1;
}

.cc-min-width-200 > * { /*child combinator*/
  min-width: 200px;
}

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<div class="flex flex-1 flex-wrap-reverse cc-min-width-200">
    <div>Hello</div>
    <div>There</div>
    <div>World</div>
</div>

Solution 6 - Javascript

If someone wants to find the last element of the row from where wrapped elements started can use the below logic. It's applicable for multiple lines as well

       window.onresize = function (event) {
            const elements = document.querySelectorAll('.borrower-detail');
            let previousElement = {};
            let rowTop = elements[0].getBoundingClientRect().top;
            elements.forEach(el => el.classList.remove('last-el-of-row'))
            elements.forEach(el => {
                const elementTop = el.getBoundingClientRect().top;

                if (rowTop < elementTop) {
                    previousElement.classList.add('last-el-of-row');
                    rowTop = elementTop;
                }

                previousElement = el;
            })
        };

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionmr.borisView Question on Stackoverflow
Solution 1 - JavascriptBrett DeWoodyView Answer on Stackoverflow
Solution 2 - Javascriptmr.borisView Answer on Stackoverflow
Solution 3 - JavascriptTropicalView Answer on Stackoverflow
Solution 4 - JavascriptsansSpoonView Answer on Stackoverflow
Solution 5 - Javascriptpbraswell352View Answer on Stackoverflow
Solution 6 - JavascriptFawad MukhtarView Answer on Stackoverflow