webdevpuneet.com

Muuri Filterable Masonry Grid — JS-Positioned Layout Snippet

Muuri Filterable Masonry Grid · Layouts · Plain HTML, CSS & JS · Live preview

CategoryLayouts

Copy & paste

Source Code

Requires
<div class="mfg-stage">
  <div class="mfg-head">
    <span class="mfg-tag">Muuri · JS-positioned layout</span>
    <h2>Project Grid</h2>
    <p>Filter buttons animate items into a re-packed layout — nothing here uses CSS grid or masonry.</p>
  </div>
  <div class="mfg-filters" id="mfgFilters">
    <button class="mfg-fbtn is-on" data-filter="all">All</button>
    <button class="mfg-fbtn" data-filter="design">Design</button>
    <button class="mfg-fbtn" data-filter="dev">Dev</button>
    <button class="mfg-fbtn" data-filter="research">Research</button>
  </div>
  <div class="mfg-grid" id="mfgGrid"></div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

JS-computed positioning
Every item is position: absolute with its (x, y) set by Muuri, not the browser's grid engine.
Animated filter transitions
grid.filter() hides non-matches and repacks the rest with a tweened layout pass.
Gap-filling packing
fillGaps: true backfills empty space left by hidden or variable-height items.
DOM-driven filtering
The filter predicate reads data-tag straight off each item's live element, no separate data model.
Variable item heights
True masonry packing where each card can be a different height, not a fixed grid row.
Configurable easing/duration
layoutDuration and layoutEasing control how the repack transition feels.
Responsive column width
Item width is set in CSS percentages, so column count adjusts at breakpoints.
Foundation for drag
The same grid can add dragEnabled: true for manual reordering with no other changes.

About this UI Snippet

Muuri Filterable Masonry Grid — Why JS-Positioned Layout Enables Smooth Filtering

Screenshot of the Muuri Filterable Masonry Grid snippet rendered live

CSS grid and CSS multi-column ("CSS masonry") both lay out items using the browser's native box model — the browser computes final positions internally and you never get to see, intercept, or animate the transition between two different layouts. Filter five items out of a CSS grid and the rest *snap* into their new slots; there is no browser API for tweening a grid-template change smoothly per item.

Muuri takes a fundamentally different approach: every grid item is position: absolute, and Muuri itself computes an (x, y) pixel offset for each item and applies it as a transform: translate(). The grid container has no native layout algorithm at all — it's just a positioned box that Muuri's JS fills.

Why absolute positioning is the whole trick

Because Muuri owns every item's coordinates directly, changing which items are visible or how they're packed is just "compute a new set of (x, y) pairs and animate each item's transform from its old pair to its new one." That's a plain CSS transition (or Muuri's own tweening, layoutDuration + layoutEasing here) applied independently per element — completely ordinary DOM animation, not a browser-internal recalculation you have no hook into.

This is the mechanical reason filtering looks the way it does in this demo: when you click "Design," Muuri doesn't reflow the grid and let items jump — it fades out and hides items that don't match, then recomputes a fresh packed layout for the ones that remain and animates every survivor to its new position, closing the gaps left behind. fillGaps: true in the layout option specifically controls the packing algorithm to backfill empty space rather than leaving holes, which matters a lot once several items are hidden.

The filter call

grid.filter(function (item) { return item.getElement().getAttribute('data-tag') === filter; })

.filter() takes a predicate function and runs it against every item currently in the grid. Items that return true stay visible and get included in the next layout pass; items that return false are hidden (Muuri toggles a muuri-item-hidden class and animates opacity/scale down) and excluded from the packing calculation entirely — they don't just become invisible, they stop taking up space, which is what allows the remaining items to flow up and fill the gap.

Reading the DOM instead of a data model

Notice the predicate reads data-tag straight off the item's live DOM element via item.getElement(). Muuri doesn't require a separate data layer — each grid item *is* a DOM node, and Muuri item objects are thin wrappers that expose that node plus position/state metadata. This keeps the filtering logic trivial: no need to keep a JS array in sync with the DOM, because the DOM already holds the category.

Reusing it

Swap fillGaps: true for false if you want strict row-order packing instead of the tightest possible fill. Combine multiple predicates for AND/OR filtering (price range and category together), or replace the button-driven filter with a text search that filters on title. Pair this with draggable dashboard widgets once you also want users to manually reorder the packed layout, not just filter it.

Step by step

How to Use

  1. 1
    Add the Muuri CDNInclude muuri.min.js from the CDN panel — a single script tag, no build step.
  2. 2
    Paste HTML, CSS, and JSNine variable-height cards build themselves from a JS array and Muuri packs them.
  3. 3
    Click a category filtergrid.filter() hides non-matching items and repacks the rest with an animated transition.
  4. 4
    Watch items fill gapsfillGaps: true in the layout option makes remaining items flow up to close empty space.
  5. 5
    Compare to CSS gridResize the window — items reflow via Muuri's JS layout, not a browser-native grid track.
  6. 6
    Add draggabilitySet dragEnabled: true to let users manually reorder items on top of the filtering.

Real-world uses

Common Use Cases

Portfolio and project grids
Filter case studies by category with every item absolutely positioned, so Muuri can animate each move between layouts.
Product catalogues
Filter products by tag or availability, with fillGaps: true backfilling space left by hidden or variable-height cards.
Blog and content indexes
Category-filter articles or posts, with the predicate reading data-tag straight off each item's element in the DOM.
Media galleries
Filter a photo grid by album or tag, pairing with the draggable Muuri dashboard widgets and Muuri photo reorder grid.
JavaScript layout engine teaching
Show why JS-computed positioning allows animation between layouts, which CSS grid and CSS columns cannot offer.

Got questions?

Frequently Asked Questions

CSS grid and multi-column layout are computed internally by the browser's layout engine, which gives you no hook to animate a transition between two different layouts — items just snap to their new grid-template position. Muuri sidesteps this by never using native grid placement at all: every item is position: absolute with coordinates Muuri computes and animates itself via transform, so a layout change is just an ordinary per-element transition.

Items whose predicate returns false get a muuri-item-hidden state, animate out (fade/scale down), and are excluded from the next layout pass entirely — they stop occupying space in the packing algorithm. Items that return true participate in a fresh layout calculation and animate to their newly packed position.

It is a packing-algorithm setting: true makes the layout algorithm backfill any empty space left by hidden or unevenly-sized items, producing the tightest possible pack. false packs more strictly in original item order, which can leave visible gaps when items of different heights are interspersed.

Each Muuri item is a thin wrapper around a real DOM node — the category is stored directly on that node as a data-tag attribute, so there's no separate JS array to keep in sync with the DOM. Reading it via item.getElement().getAttribute('data-tag') keeps the DOM as the single source of truth.

Set dragEnabled: true in the Muuri constructor options — filtering and dragging are independent features that both operate on the same absolutely-positioned item model, so enabling drag doesn't require changing the filter logic at all. See the draggable dashboard widgets snippet for the drag-specific configuration.

Yes — item width is set in CSS (here, 33.333% per item, 50% on narrow screens via a media query), and Muuri recalculates the packed layout whenever the grid is refreshed or the window resizes, since it measures actual rendered item dimensions rather than assuming a fixed grid track size.