Search code examples
cssflexbox

Elements within a flex column with wrapping enabled don't retain content height when including nested aspect ratio based elements


Today I had to build a rather complex layout that required stacking flexed items vertically, with aspect-ratio bound images within. While developing this, I found that when the flex container is set to flex-wrap: wrap;, the elements within that use aspect-ratio or even the pseudo-element method (i.e. &::before { padding-bottom: 56.25%; }) cause the flexed items to escape their containers.

Setting flex-wrap: none; is a fine solution for my purposes today, but I was very confused as to why this is happening, and even across multiple browsers (tested in Firefox, Chrome, and Edge). Is this intentional behavior? If so, why does it work this way?

https://codepen.io/JacobDB/pen/bGZajOX

/**
 * Button for testing
 */
const BUTTON = document.querySelector("button");
const ROW    = document.querySelector(".row");

BUTTON.addEventListener("click", (e) => {
    e.preventDefault();
    
    if (ROW.classList.contains("flex-wrap")) {
        ROW.classList.remove("flex-wrap");
        BUTTON.innerHTML = "flex-wrap: none;";
    } else {
        ROW.classList.add("flex-wrap");
        BUTTON.innerHTML = "flex-wrap: wrap;";
    }

}, { passive: false });
.row {
    display: flex;
    flex-direction: column;
}

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

figure {
    aspect-ratio: 16 / 9;
}

figure.pseudo-element {
    aspect-ratio: auto;
}

figure.pseudo-element::after {
    content: "";
    display: block;
    padding-bottom: 56.25%;
}

/**
 * Pretty it up a bit
 */
:root { font-family: sans-serif; }
button { margin-bottom: 1em; }
.row { border: 2px dotted blue; padding: 1em; }
.col { border: 2px dotted red; padding: 1em; }
.col:not(:first-child) { border-top: 0; }
article { border: 2px dotted green; }
figure { position: relative; }
<button>
    flex-wrap: wrap;
</button>

<div class="row flex-wrap">
    <div class="col">
        <article>
            <figure>
                Figure 1
            </figure>
        </article>
    </div>
    <div class="col">
        <article>
            <figure class="pseudo-element">
                Figure 2 (Pseudo Element)
            </figure>
        </article>
    </div>
    <div class="col">
        <article>
            <figure>
                Figure 3
            </figure>
        </article>
    </div>
</div>


Solution

  • First of all, update the align-items to use something different from stretch (the default value) and both will behave the same

    /**
     * Button for testing
     */
    const BUTTON = document.querySelector("button");
    const ROW    = document.querySelector(".row");
    
    BUTTON.addEventListener("click", (e) => {
        e.preventDefault();
        
        if (ROW.classList.contains("flex-wrap")) {
            ROW.classList.remove("flex-wrap");
            BUTTON.innerHTML = "flex-wrap: none;";
        } else {
            ROW.classList.add("flex-wrap");
            BUTTON.innerHTML = "flex-wrap: wrap;";
        }
    
    }, { passive: false });
    .row {
        display: flex;
        flex-direction: column;
        align-items: start;
    }
    
    .flex-wrap {
        flex-wrap: wrap;
    }
    
    figure {
        aspect-ratio: 16 / 9;
    }
    
    figure.pseudo-element {
        aspect-ratio: auto;
    }
    
    figure.pseudo-element::after {
        content: "";
        display: block;
        padding-bottom: 56.25%;
    }
    
    /**
     * Pretty it up a bit
     */
    :root { font-family: sans-serif; }
    button { margin-bottom: 1em; }
    .row { border: 2px dotted blue; padding: 1em; }
    .col { border: 2px dotted red; padding: 1em; }
    .col:not(:first-child) { border-top: 0; }
    article { border: 2px dotted green; }
    figure { position: relative; }
    <button>
        flex-wrap: wrap;
    </button>
    
    <div class="row flex-wrap">
        <div class="col">
            <article>
                <figure>
                    Figure 1
                </figure>
            </article>
        </div>
        <div class="col">
            <article>
                <figure class="pseudo-element">
                    Figure 2 (Pseudo Element)
                </figure>
            </article>
        </div>
        <div class="col">
            <article>
                <figure>
                    Figure 3
                </figure>
            </article>
        </div>
    </div>

    Now take the same example, keep flex-wrap: wrap and then toggle the align-items. You will notice that the height of containers will remain the same while the child items overflow.


    The use of flex-wrap define the nature of the container, it's either "single line" or "multi-line"

    A flex container can be either single-line or multi-line, depending on the flex-wrap property: ref

    Even if you have one line (one column in your case) your container is still consider a "multi-line" one if flex-wrap is set to wrap and this affect the sizing algorithm of the lines and the items.

    When using a "single line" configuration, calculating the size is easy.

    In a single-line flex container, the cross size of the line is the cross size of the flex container

    The line will have the size of your container (the content play no role here) then each container will stretch to fill that line (full width in your case) then the elements inside will get sized considering their ratio and will logically expand the height of their container.

    When using a "multi line" configuration, it's another story because the size of each line will now depend on the content. I won't detail all the algorithm but the idea is that the browser need to first find a size for the lines (based on the content) then the elements will fit that size and their height will get calculated. Then the stretch alignment will expand the size of the lines. This will logically expand the size of the elements inside them BUT we don't calculate the height of the container again because it was already done in a previous step.

    It may look a bit strange and not intuitive because you may think that in your particular case, we should expand the heights (which is true) but in some situation, changing the height will lead to an infinite loop that's why it won't get updated and the child items will overflow.


    The important thing you should keep in mind is that flex-wrap do more than what you might think. It's doesn't only enable wrapping but it changes the algorithm used to size the elements and, as you noticed, it can make a difference.

    Some related questions with a similar situation:

    flex-wrap causes children to double in height

    Why is the flex-wrap of my container causing my auto-fill grid to add extra empty tracks row?