67

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 jquery event listen on position changed

But still terribly.

tanguy_k
  • 11,307
  • 6
  • 54
  • 58
mr.boris
  • 3,667
  • 8
  • 37
  • 70
  • you **can't** detect wrapping in CSS- whether you are using `flexbox` or `float`, you'd better off with `media queries`... – kukkuz Oct 13 '16 at 04:43
  • 2
    Thank you for the answer. May be there is a simple way to detect it by JS? I think there is no point to "catch" floating breaking/wrapping point for flex elements through media query. – mr.boris Oct 13 '16 at 05:11
  • 1
    What is causing the items to wrap - a screen resize? more elements being added? – Brett DeWoody Oct 13 '16 at 05:17
  • Yes. Screen resize causing elements to wrap. I want to position elements appropriately, add some margin/padding to aling them and do some other stuff after each element has been wrapped. All in all if I would be able to detect flex wrapping it wuld be very useful! It is very strange that it is impossible to detect breaking point of flex wrapping. – mr.boris Oct 13 '16 at 05:29
  • What is causing the elements to wrap in the first place? Do they have a `min-width`, or the content prevents them from shrinking so they wrap? – Brett DeWoody Oct 13 '16 at 05:32
  • Сontent prevents them from shrinking so they wrap. Simple question. We have three elements in row with different width. When we resize container last element wraps. How to align last element to the end of a row or align it to the second element? – mr.boris Oct 13 '16 at 05:46
  • This is biggest issue I always run into. Media queries sucks a lot, flex is nice for simple wrapping but many times you want to change alignment for wrapped elements :/ ano no real solution for this yet. – Jurosh Apr 01 '19 at 13:16
  • I feel you bro, welcome to css in 2k19. Flexbox definitely needs something like `:wrapped` pseudo selector to solve this problem.I think even css-grid won't solve this problem but mb will reduce media queries count in your css style. – mr.boris Apr 02 '19 at 14:35
  • For the case of adding margins, you could use a "spacer" element instead. It's ugly but it works. Flex wrapping is one feature that makes flex better than grid for some kinds of layouts. – kumar303 Jul 17 '19 at 21:59

7 Answers7

28

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>
Dai
  • 141,631
  • 28
  • 261
  • 374
Brett DeWoody
  • 59,771
  • 29
  • 135
  • 184
  • 4
    Thank you for your help! I think this snippet will be very useful for everyone. All in all I think that flexbox needs new css property such as "wrapped" or something like this to get rid of js spaghetti. Also this snippet could be improved. For example to detect all wrapped element instantly when page is loaded and haven't been resized yet. And it is rewrites previous css style of each element now. – mr.boris Oct 13 '16 at 09:05
  • How would you handle the following scenario - let's say you have 3 items. The first item goes full-width. So the 2nd and 3rd items are on the 2nd row. Would the 3rd item be considered wrapped? – Brett DeWoody Oct 13 '16 at 14:07
  • Nice question. 3-rd element wouldn'tbe considered as wrapped relatively to 2-nd item. The best way in this case is to check whether the parent element of the class with "display: flex" css property. Then you can check each child item that has ben wrapped. I need to improve snippet. – mr.boris Oct 13 '16 at 16:38
9

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;
   });
}
mr.boris
  • 3,667
  • 8
  • 37
  • 70
  • If the flex-direction is column, then change the if statements to: if (offset_top < offset_top_prev) {} else {}. Also, if using bootstrap 4/5, then the selector should be $('.d-flex > *') to select all flex items. – carlin.scott Oct 26 '21 at 19:50
4

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.

Tropical
  • 126
  • 1
  • 4
  • We should be using `for(of)` instead of `Array.prototype.forEach` btw. Using `.forEach` introduces closures and has worse performance. Also, reading _and writing_ to the DOM in a single loop is really bad for UX because the browser needs to perform layout operations between each loop iteration. Instead have two separate loops: the first gathers size information for all relevant elements, the second loop then modifies the DOM (like toggling the `wrapped` class) without reading any properties that would trigger a relayout. – Dai Aug 08 '21 at 10:35
  • Yes, I wrote this a while ago when I was unaware of the downsides of `Array.prototype.forEach()`. Should probably rewrite my solution. – Tropical Aug 13 '21 at 05:56
  • Another thing worth noting is that if the parent container has a position: relative (or absolute), then the offsetTop of the direct child will be 0 if it aligns to the top of its parent. Can just set position:relative and check child offsetTop against 0. https://stackoverflow.com/questions/11634770/get-position-offset-of-element-relative-to-a-parent-container – Harry Robbins Sep 19 '22 at 19:09
2

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';
            }
        }
    }
}
sansSpoon
  • 2,115
  • 2
  • 24
  • 43
1

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

Avoid using media queries for this type of task, it's too unstable. Think of the work involved when another flex item is added, or the text length changes, and you have to rejig those media queries.

Instead consider using the Resize Observer API.

A list of links in a flex box:

<ul class=flex_box role=list>
    <li><a href="#/">Fake link 1</a></li>
    <li><a href="#/">Fake link 2</a></li>
    <li><a href="#/">Fake link 3</a></li>
    <li><a href="#/">Fake link 4</a></li>
    <li><a href="#/">Fake link 5</a></li>
</ul>

CSS:

.flex_box {
    display: flex;
    flex-wrap: wrap;
    padding: 0;
    gap: 1rem;
}

/* Optional coloring prettiness */
.flex_box > * {
    background-color: #000;
    color: #fff;
}
.flex_box-wrapped > .flex_item-wrapped {
    background-color: #333
}

Upon wrapping, the script adds class flex_box-wrapped to the flex container, and flex_item-wrapped to each flex item which has wrapped.

This is achieved by testing the top position of each item against the first item.

When the browser width (or height, or font-size) changes the Resize Observer API recalculates and amends each class name accordingly.

// An IFEE here, but you can roll your own:
const flexBoxWrapDetection = (_ => {
  'use strict';

  const flexBoxQuery = '.flex_box';
  const boxWrappedClass = 'flex_box-wrapped';
  const itemWrappedClass = 'flex_item-wrapped';


  // Rounded for inline-flex sub-pixel discrepencies:
  const getTop = item => Math.round(item.getBoundingClientRect().top);


  const markFlexboxAndItemsWrapState = flexBox => {

    // Acts as a throttle, 
    // Prevents hitting ResizeObserver loop limit,
    // Optimal timing for visual change:
    requestAnimationFrame(_ => {

      const flexItems = flexBox.children;

      // Needs to be in a row for the calculations to work
      flexBox.setAttribute('style', 'flex-direction: row');

      const firstItemTop = getTop(flexItems[0]);
      const lastItemTop = getTop(flexItems[flexItems.length - 1]);

      // Add / remove wrapped class to each wrapped item
      for (const flexItem of flexItems) {
        if (firstItemTop < getTop(flexItem)) {
          flexItem.classList.add(itemWrappedClass);
        } else {
          flexItem.classList.remove(itemWrappedClass);
        }
      }

      // Remove flex-direction:row used for calculations
      flexBox.removeAttribute('style');

      // Add / remove wrapped class to the flex container
      if (firstItemTop >= lastItemTop) {
        flexBox.classList.remove(boxWrappedClass);
      } else {
        flexBox.classList.add(boxWrappedClass);
      }
    });
  };


  // Each flex box with the class .flex_box:
  const flexBoxes = document.querySelectorAll(flexBoxQuery);
  for (const flexBox of flexBoxes) {

    markFlexboxAndItemsWrapState(flexBox);

    // Listen for dimension changes on the flexbox
    new ResizeObserver(entries =>
      entries.forEach(entry => markFlexboxAndItemsWrapState(entry.target))
    ).observe(flexBox);

  }
})();

There's a demo CodePen: Flex-wrap detection script which also shows how to switch between horizontal and vertical as soon as the flex box wraps or unwraps.

The demo also works with direction: rtl; and / or flex-wrap: row-reverse;.

2kool2
  • 21
  • 2
  • Thank you, it's nice to have one more example here. Seems there is a little bug in your CodePen demo with the second list: when the parent box is resized then every list item is wrapped but not all of them are assigned the new `.flex_item-wrapped` class. So, all items are wrapped but only a few of them have the wrapped class. – mr.boris Apr 21 '23 at 09:35
  • 1
    @mr.boris Thanks for raising the issue. It doesn't affect the example above but I've amended the CodePen version to add `.flex_item-wrapped` class to every item on the "switched" example. – 2kool2 Apr 25 '23 at 20:26
0

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;
            })
        };
Fawad Mukhtar
  • 780
  • 5
  • 9