GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

CSS Masonry Layout: 9 Ways to Pack Uneven Cards

A css masonry layout of one columns or grid line scrambles order and teleports cards on resize — scenes 01, 02 and 04 fix it by shortest-column packing, row

Rearrange All Nine Brick Layouts Right on This Page

Auto-plays · click a tile to jump to its section · all nine in one zip

CSS Masonry Layout: 9 Grids Where Column Heights, Row Units and Springs Close the Gaps

Each scene builds a wall up to 440px wide and 216px tall and stacks it with gradient cards that carry nothing but a number. Card sizes come from a seeded random sequence, so a given moment always draws the same frame, and the buttons in the bar above the wall change the column count, the mode or the filter.

The nine scenes share a two-layer structure: first work out each card's target spot, then let the screen follow it on its own schedule. The spot rules differ from scene to scene, and 02, 03, 05 and 07 do not use the shortest-column placement of pack at all. Only 04 holds a velocity and a stiffness, so it is the one true spring; 09 integrates gravity, 01, 02 and 06 close a fraction of the remaining distance each frame, 07 adds speed times elapsed time, 08 runs a timed easing curve, and 03 and 05 compute the layout in place. No scene has a CSS transition or @keyframes, a library, an outside request or Math.random.

For a gallery of mixed image ratios start with 03, for a list with filter buttons start with 08, and for a responsive screen whose width keeps changing start with 04. Slides, zoom and lightbox effects belong to 9 CSS Image Gallery Grid Effects and CSS Image Gallery Animation 9 One Line, Copy-Paste; this page only decides which spot each card takes.

Code for Each Technique Behind a CSS Masonry Grid

The snippets are copied from consecutive lines of each scene's index.html script block, 25 lines at most. A css brick layout is mostly this placement math with a follow-along layer on top, so reading the math first tells you what a changed value will do.

01Shortest-Column Packing That Drops Each Card on the Lowest Column

The cards are read in order and each one lands on whichever column is lowest at that moment (the left one on a tie), then that column grows by the card height plus the 8px gap. It suits pin boards and portfolio grids, and the thin bars under the columns show where every column ends. Cumulative area came to 47.7%, with motion in 23/23 frames.

Shortest-Column Packing That Drops Each Card on the Lowest Column
Open in a new tab

01 · Shortest-Column Packing That Drops Each Card on the Lowest Column — Measured area 47.727% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

COLUMNSGAPSETTLESEED
    function pack(hs, cols, cw, gap) {
        const colH = [];
        for (let k = 0; k < cols; k += 1) colH.push(0);
        const out = [];
        for (let i = 0; i < hs.length; i += 1) {
            let best = 0;
            for (let k = 1; k < cols; k += 1) {
                if (colH[k] < colH[best] - 0.5) best = k;
            }
            out.push({ x: best * (cw + gap), y: colH[best] });
            colH[best] += hs[i] + gap;
        }

02A Column Layout Reordered to Read Left to Right

Fill-down puts card numbers down each column and scrambles reading order, row-wise picks the column from the card index modulo the column count, and balanced goes row-wise until a column gets too tall and then sends the card to the lowest one. With the same nine cards the column ends differ by 5px, 72px and 30px (three columns, 424px wide), and balanced swaps only cards 5 and 6. This scene reached 53.1% area and 19/23 moving frames.

A Column Layout Reordered to Read Left to Right
Open in a new tab

02 · A Column Layout Reordered to Read Left to Right — Measured area 53.068% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

COLUMNSBALANCEGAPFOLLOW
            const c = cards[i];
            const h = c.ratio * cw;
            let col = i % COLUMNS;
            if (mode === 0) {
                col = Math.min(COLUMNS - 1, Math.floor(i / perCol));
            } else if (mode === 2) {
                let low = 0;
                for (let k = 1; k < COLUMNS; k += 1) {
                    if (colH[k] < colH[low]) low = k;
                }
                if (colH[col] - colH[low] > BALANCE * h) col = low;
            }
            c.w = cw;
            c.h = h;
            c.tx = col * (cw + GAP);
            c.ty = colH[col];
            colH[col] += h + GAP;

03A Grid Span Computed from Each Image Ratio

Placement belongs to a real CSS grid, and the script only works out span = ceil((h + 8) / 16) and writes it to gridRowEnd. It fits galleries with mixed image ratios, and the number slot inside each card shows its span. Measured at 60.2% area, it moved in 23/23 frames.

A Grid Span Computed from Each Image Ratio
Open in a new tab

03 · A Grid Span Computed from Each Image Ratio — Measured area 60.184% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ROW_UNITGAPSPAN_BIASCOLUMNS
    function spanOf(h) {
        return Math.ceil((h + GAP) / (ROW_UNIT + GAP)) + SPAN_BIAS;
    }
    function relayout() {
        wall.style.width = Math.round(root.clientWidth * frac) + "px";
        const cw = (wall.clientWidth - GAP * (COLUMNS - 1)) / COLUMNS;
        for (let i = 0; i < cards.length; i += 1) {
            const n = spanOf(cw * cards[i].ratio);
            if (n !== spans[i]) {
                spans[i] = n;
                cards[i].el.style.gridRowEnd = "span " + n;
                const badge = cards[i].el.firstChild;
                if (badge) badge.textContent = String(n);
            }
        }
        spanEl.textContent = String(spans[0]);
        shownFrac = frac;
        ready = true;
    }

04A FLIP Reflow Where Cards Spring Into Place When Columns Change

FLIP means measuring where a card was and where it will be and keeping that difference as an offset that relayout leaves and kick pulls to zero on every frame with a velocity and a stiffness. The width difference is undone with scale as well, so going from 3 to 4 columns starts the cards stretched by +0.36, and changing the column count again mid-flight continues from the current screen position without a jump. It scored 48.4% area with 19/23 frames moving.

A FLIP Reflow Where Cards Spring Into Place When Columns Change
Open in a new tab

04 · A FLIP Reflow Where Cards Spring Into Place When Columns Change — Measured area 48.372% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

STIFFNESSDAMPINGSTAGGERAREA
    function kick(o, v, f) {
        const nv = (v - STIFFNESS * o * f) * Math.pow(DAMPING, f);
        return [o + nv * f, nv];
    }
        for (let i = 0; i < cards.length; i += 1) {
            const c = cards[i];
            if (ready && !reduce) {
                // First: the offset is the new spot and size subtracted from the spot and size on screen now.
                c.ox = c.tx + c.ox - at[i].x;
                c.oy = c.ty + c.oy - at[i].y;
                c.osx = (c.tw * (1 + c.osx)) / cw - 1;
                c.osy = (c.th * (1 + c.osy)) / hs[i] - 1;
                c.wait = i * STAGGER;
            }
            c.tx = at[i].x;
            c.ty = at[i].y;
            c.tw = cw;
            c.th = hs[i];
            c.w = cw;
            c.h = hs[i];
            if (!ready || reduce) {
                c.ox = 0;
                c.oy = 0;
                c.osx = 0;
                c.osy = 0;
            }
        }

05Justified Rows Scaled to Fill Each Line Exactly

Aspect ratios are added up until the line width reaches the wall width, the line is cut there, and one factor s multiplies that line's height so the right edge lands flush. A full line always has s ≤ 1, so it can only shrink, which suits photo albums and result lists. The scene's area was 44.8%, and 19/23 frames showed movement.

Justified Rows Scaled to Fill Each Line Exactly
Open in a new tab

05 · Justified Rows Scaled to Fill Each Line Exactly — Measured area 44.764% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

TARGET_HGAPMAX_STRETCHLERP
        while (i < cards.length) {
            const from = i;
            let sum = 0;
            while (i < cards.length) {
                sum += cards[i].ratio;
                i += 1;
                if (sum * th + GAP * (i - from - 1) >= width) break;
            }
            const n = i - from;
            let s = (width - GAP * (n - 1)) / (sum * th);
            // If the last row is too short, keep the target height instead of stretching it.
            if (s > 1 + MAX_STRETCH) s = 1;
            const rowH = th * s;
            let x = 0;
            for (let j = from; j < i; j += 1) {
                const c = cards[j];
                c.w = cards[j].ratio * rowH;
                c.h = rowH;
                c.x = x;
                c.y = y;
                x += c.w + GAP;
            }
            y += rowH + GAP;
            rows += 1;
        }

06A Lens Layout Where Neighbors Make Way Near the Pointer

The distance d between a card center and the pointer goes through a Gaussian falloff to set a target scale and a sidestep distance, and the live values chase them by exponential approach. Near cards swell and far ones press down slightly, a good match for image boards scanned by hover. Area 54.4%, 23/23 frames in motion.

A Lens Layout Where Neighbors Make Way Near the Pointer
Open in a new tab

06 · A Lens Layout Where Neighbors Make Way Near the Pointer — Measured area 54.377% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

LENS_RADIUSGAINFALLOFFLERP
            if (has) {
                const dx = c.tx + c.w / 2 - px;
                const dy = c.ty + c.h / 2 - py;
                const d = Math.sqrt(dx * dx + dy * dy);
                const g = Math.exp(-FALLOFF * (d / LENS_RADIUS) * (d / LENS_RADIUS));
                ds = 1 + GAIN * g - 0.06 * (1 - g);
                if (d > 1) {
                    ux = (dx / d) * PUSH * 4 * g * (1 - g);
                    uy = (dy / d) * PUSH * 4 * g * (1 - g);
                }
                lift = g;
                if (d < bestD) {
                    bestD = d;
                    best = i;
                }
            }
            const k = reduce ? 1 : 1 - Math.pow(1 - LERP, f);
            c.s += (ds - c.s) * k;
            c.ox += (ux - c.ox) * k;
            c.oy += (uy - c.oy) * k;

07An Endless Brick Wall Whose Columns Drift in Opposite Directions

Columns alternate direction, each speed is SPEED × DEPTH[k], the position is added directly, and wrapping by the column height hides the seam. At the current width every column holds two copies of its cards, so 20 cards become 40 DOM nodes, and the result fits landing backgrounds and logo walls. At 62.3% it covers the most area of the nine, with motion in 23/23 frames.

An Endless Brick Wall Whose Columns Drift in Opposite Directions
Open in a new tab

07 · An Endless Brick Wall Whose Columns Drift in Opposite Directions — Measured area 62.272% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SPEEDDEPTHCOLUMNSGAP
            for (let n = 0; n < copies; n += 1) {
                let y = n * total;
                for (let i = 0; i < PER; i += 1) {
                    const h = ratios[k * PER + i] * cw;
                    const el = document.createElement("div");
                    el.className = CLS.card;
                    el.setAttribute("data-c", String((k * PER + i) % 6));
                    el.innerHTML = "<i>" + (k * PER + i + 1) + "</i>";
                    if (n > 0) el.setAttribute("aria-hidden", "true");
                    el.style.width = cw.toFixed(1) + "px";
                    el.style.height = h.toFixed(1) + "px";
                    el.style.transform = "translate3d(0," + y.toFixed(1) + "px,0)";
                    col.appendChild(el);
                    y += h + GAP;
                }
            }

08A Shuffle Where Cards Move in Sequence When a Filter Is Chosen

Only the cards that pass the filter are packed again by the shortest-column rule, and each one starts rank × STAGGER later and moves on a timed easing curve. Cards that fail the filter fold in place to 0.5 scale and 0 opacity, which suits category-filter galleries. It reached 50.9% area and 22/23 moving frames.

A Shuffle Where Cards Move in Sequence When a Filter Is Chosen
Open in a new tab

08 · A Shuffle Where Cards Move in Sequence When a Filter Is Chosen — Measured area 50.885% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

STAGGERDURATIONEASE_POWERCOLUMNS
            if (visible(c)) {
                c.tx = at[rank].x;
                c.ty = at[rank].y;
                c.ds = 1;
                c.dop = 1;
                c.t0 = now + rank * STAGGER;
                rank += 1;
            } else {
                c.ds = 0.5;
                c.dop = 0;
                c.t0 = now;
            }

09Bricks That Fall, Bounce and Stack Into Place

Each card starts DROP_DELAY later than the one before, falls under gravity, rebounds at 0.32 of its impact speed and squashes vertically before it settles. First landings are spread between 233ms and 1517ms and everything is still by 1850ms, so it works as an entrance animation right after loading. Area 46.3%, 22/23 frames moved.

Bricks That Fall, Bounce and Stack Into Place
Open in a new tab

09 · Bricks That Fall, Bounce and Stack Into Place — Measured area 46.294% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

GRAVITYBOUNCESQUASHDROP_DELAY
        for (let s = 0; s < n; s += 1) {
            for (let i = 0; i < cards.length; i += 1) {
                const c = cards[i];
                c.sq *= Math.exp(-14 * h);
                if (c.rest === 1 || ts - startAt < i * DROP_DELAY) continue;
                c.vy += GRAVITY * h;
                c.y += c.vy * h;
                if (c.y >= c.ty) {
                    const hit = c.vy;
                    c.y = c.ty;
                    if (hit > 90) {
                        c.vy = -hit * BOUNCE;
                        c.sq = Math.max(c.sq, Math.min(0.3, (SQUASH * hit) / 4200));
                    } else {
                        c.vy = 0;
                        c.rest = 1;
                    }
                }
            }
        }

Where the Values Change the Layout — Column Count, Spring Stiffness, Lens Radius

Three values change the shape most. The numbers below come from calling the same functions of the source in node with a 440px wall, so they are separate from the numbers measured in the browser render.

More Columns Make Narrower Cards and Level Column Ends

Changing COLUMNS in 01 from 3 to 5 narrows each card from 141px to 82px, lowers the longest column end from 190px to 66px and shrinks the gap between column ends from 54px to 36px. The same nine cards spread wider, so each column stacks less height.

A Stiffer Spring Doubles the Overshoot and Settles Later

Raising STIFFNESS in 04 from 0.12 to 0.3 with DAMPING untouched lifts the overshoot on a 100px move from 21px to 41px. The first arrival stays near 0.28 seconds, but staying within 1px for good slips from 0.45 to 0.55 seconds.

A Wider Lens Releases the Pressed Cards

Widening LENS_RADIUS in 06 from 140 to 240 with the pointer at the wall center grows the cards above 1.1 times from 5 of 12 to 6 of 12, and the largest scale from 1.31 to 1.45. Cards pressed below 0.97 drop from 6 to 0, so the whole wall swells together.

The table below gathers what the browser render measured. The smallest area is 05 at 44.8% and the largest is 07 at 62.3%; five scenes moved in fewer than 23 frames, namely 02 (19), 04 (19), 05 (19), 08 (22) and 09 (22), and the cause was not checked.

Scene Cumulative area Mean intensity Moving frames
01 Shortest column 47.7% 102.9 23/23
02 Three column rules 53.1% 58.7 19/23
03 Grid span 60.2% 123.3 23/23
04 FLIP spring 48.4% 115.0 19/23
05 Justified rows 44.8% 65.5 19/23
06 Lens 54.4% 98.4 23/23
07 Drifting columns 62.3% 159.2 23/23
08 Filter shuffle 50.9% 63.6 22/23
09 Falling bricks 46.3% 126.8 22/23

Where It Breaks — Hundreds of Cards, Narrow Screens, Reduced-Motion Settings

Card Height Is a Ratio Times Width, So Real Cards Need Measuring

Seven scenes (01, 02, 03, 06, 07, 08 and 09) set height as ratio × column width. Scene 04 uses ratio × AREA / column width, so a card grows taller as it narrows, and 05 multiplies the ratio by the row height to get a width instead. A card holding an image or text gets its height from its content, so draw it once, measure that height and hand it to pack.

The Shortest Column Throws Away Reading Order

pack in 01 looks only at heights, so the numbers hop between columns. If the list must read across, use the row-wise rule of 02 and pay for it with column ends that drift from 5px to 72px apart, while the balanced rule settles midway at 30px.

A Grid Span Leaves a Gap Under Every Card

03 rounds up so that span × 16 - 8 covers the card, which leaves up to 15px of slack per card and more as the row unit grows. When the width changes the spans are recomputed, and the card heights jump one row unit with no interpolation.

The FLIP Width Compensation Squeezes Text for a Moment

04 undoes the width difference with scale, so 3 to 4 columns starts +0.36 stretched and the digits and corners are distorted until they settle. Raising stiffness alone turns a 21px overshoot into 41px.

The Last Justified Row Does Not Reach the Right Edge

In 05 the last row is left unstretched when its factor would exceed 1.35, so its right edge stays short. With a target height of 44, the five cards on row two end at 237.6px, while at 55 both rows of seven fit 440px exactly.

The Lens Turns On Differently by Touch and by Keyboard

When the pointer is not a mouse, 06 switches the lens off at pointerup, and for the keyboard the 12 cards have tabIndex, so the lens only sits on a card center while that card holds focus. A magnified card is clamped inside the wall, which blocks the push direction at the corners.

A Width Change Rebuilds the DOM

07 calls build() and recreates every card when the width changes. Forty nodes cost nothing, but several hundred cards could stutter while the window is being dragged.

Falling Bricks Never Collide

09 has no collision test between cards of the same column, and each stops only at its own target height. Only the gaps in arrival time prevent overlap, so a smaller DROP_DELAY lets a card on top overlap the one still bouncing below.

Editing the Values Yourself to Find the Edges

Change COLUMNS in 01, STIFFNESS in 04 and LENS_RADIUS in 06 one at a time and the numbers above show up where they flip. The sources of all nine scenes and the React version are in the archive that opens with the password vbqsakg9.

Three Variants and Accessibility

Name Value Feel
Change the column count 01 COLUMNS 3 → 5 Cards narrow from 141px to 82px, the longest column end drops from 190px to 66px and the gap between column ends shrinks from 54px to 36px.
Change the spring stiffness 04 STIFFNESS 0.12 → 0.3 First arrival stays near 0.28 s, but overshoot doubles from 21px to 41px and settling moves from 0.45 s to 0.55 s.
Change the lens radius 06 LENS_RADIUS 140 → 240 Cards above 1.1 times go from 5 to 6, the largest scale from 1.31 to 1.45, and pressed cards from 6 to 0.

All three sets of numbers were measured by calling the source functions in node.

Eight scenes are driven by real <button> elements; 06 has none, and its 12 cards take tabIndex so Tab reaches them. There is no keydown handler and no preventDefault in any of the nine files, so the keyboard works only through the default Enter and Space of a button. The status line's aria-live stays off during the demonstration and turns polite once input arrives, and all nine scenes listen to a ResizeObserver for width changes and recompute the spots.

With prefers-reduced-motion set, none of the nine starts its demonstration or animation loop and the finished layout is drawn once. Input is applied without interpolation, and the Stack again button in 01 and the Pause button in 07 are hidden. In 08 a filtered-out card is only transparent and stays in the DOM, so a link card version needs hidden or inert added separately.

Is CSS Masonry with Flexbox Alone Enough?

A css masonry layout with flexbox usually means pouring cards into columns one after another, which is the order of the fill-down rule in 02: the column ends are the closest, 5px apart, but the numbers run down each column and reading order breaks the most. Keeping the row-wise order means choosing a column per card, as the other rules in 02 do, and fitting different heights onto one line needs the line-by-line calculation of 05. All nine files place cards by calculation, and only 03 hands the placement to CSS after working out the row spans.

Each scene suits a different job. 01 fits a pin board, 02 a card list where reading order matters, 03 a gallery of mixed image ratios, 04 responsive transitions, 05 a photo album, 06 an image board scanned by hover, 07 a landing background, 08 a category-filter gallery and 09 an entrance animation. To lay out a whole body of work, continue with Portfolio Gallery HTML CSS: 9 UI Patterns, and for opening a card into a large view, see CSS Lightbox Gallery Animation — 9 Transitions. The observer that reports width changes is documented in ResizeObserver, and the basic rules of splitting content into columns in CSS multi-column layout.

Frequently Asked Questions Before Reusing a CSS Masonry Layout CodePen Example

Where do I change values if I expect a css masonry grid generator?

This page has no form fields, so edit the const values inside the <script> of each scene's index.html instead. COLUMNS is the column count, GAP the spacing and SEED the random sequence that picks card heights, and the names on the chips are the values worth changing in that scene.

Does it still work when the card count grows?

The nine scenes were measured with 8 to 20 cards and nothing beyond that was checked. pack in 01 compares each card with every column, so its cost grows in proportion to the card count, but a scene that rebuilds the DOM such as 07 needs its own measurement at several hundred cards.

Do I need a library or images?

No. None of the nine files makes an outside request, and the scenes run on gradient boxes that carry only a number, so a single scene works on its own once you take both its index.html and its style.css.

Enter the archive password

The password is inside this article. You will find it as you read.