Muuri: Responsive, sortable grid layouts

07/03/2019
layout

Muuri: Responsive, sortable grid layouts

Muuri creates responsive, sortable, filterable and draggable grid layouts. They manage to pack a lot of features into a small package. Muuri is basically Packery + Masonry + Isotope and jQuery UI sortable. Muuri’s layout system allows positioning the grid items within the container in pretty much any way imaginable. The default “First Fit” bin packing layout algorithm generates similar layouts as Packery and Masonry. The implementation is heavily based on the “maxrects” approach as described by Jukka Jylänki in his research A Thousand Ways to Pack the Bin. However, you can also provide your own layout algorithm to position the items in any way you want.

Muuri

gzip size npm

Muuri is a JavaScript layout engine that allows you to build all kinds of layouts and make them responsive, sortable, filterable, draggable and/or animated. Comparing to what's out there Muuri is a combination of Packery, Masonry, Isotope and Sortable. Wanna see it in action? Check out the demo on the website.

Muuri's default "First Fit" bin packing layout algorithm generates layouts similar to Packery and Masonry. The implementation is heavily based on the "maxrects" approach as described by Jukka Jylänki in his research A Thousand Ways to Pack the Bin. If that's not your cup of tea you can always provide your own layout algorithm to position the items as you wish.

Muuri uses Web Animations for animations and Hammer.js to handle dragging. And if you're wondering about the name of the library "muuri" is Finnish meaning a wall.

Features

  • Fully customizable layout
  • Drag & drop (even between grids)
  • Nested grids
  • Fast animations
  • Filtering
  • Sorting

Table of contents

Getting started

1. Get Muuri

Download:

  • muuri.js - for development (not minified, with comments).
  • muuri.min.js - for production (minified, no comments).

Or link directly:

<script src="https://unpkg.com/muuri@0.7.1/dist/muuri.min.js"></script>

Or install with npm:

npm install muuri

2. Get the (optional) dependencies

Hammer.js

Muuri uses Hammer.js (v2.0.0+) to handle all the drag events. It is an optional dependency and only required if you need Muuri's dragging capabilities. In other words, if you set dragEnabled option to true you need Hammer.js.

Web Animations Polyfill

Muuri uses Web Animations to handle all the animations by default. If you need to use Muuri on a browser that does not support Web Animations you need to use a polyfill.

3. Add the script tags

Add Muuri on your site and make sure to include the optional dependencies (if needed) before Muuri.

<script src="https://unpkg.com/web-animations-js@2.3.1/web-animations.min.js"></script> <script src="https://unpkg.com/hammerjs@2.0.8/hammer.min.js"></script> <script src="https://unpkg.com/muuri@0.7.1/dist/muuri.min.js"></script>

4. Add the markup

  • Every grid must have a container element.
  • Grid items must always consist of at least two elements. The outer element is used for positioning the item and the inner element (first direct child) is used for animating the item's visibility (show/hide methods). You can insert any markup you wish inside the inner item element.
<div class="grid">    <div class="item">     <div class="item-content">       <!-- Safe zone, enter your custom markup -->       This can be anything.       <!-- Safe zone ends -->     </div>   </div>    <div class="item">     <div class="item-content">       <!-- Safe zone, enter your custom markup -->       <div class="my-custom-content">         Yippee!       </div>       <!-- Safe zone ends -->     </div>   </div>  </div>

5. Add the styles

  • The container element must be "positioned" meaning that it's CSS position property must be set to relative, absolute or fixed. Also note that Muuri automatically resizes the container element's width/height depending on the area the items cover and the layout algorithm configuration.
  • The item elements must have their CSS position set to absolute and their display property set to block. Muuri actually enforces the display:block; rule and adds it as an inline style to all item elements, just in case.
  • The item elements must not have any CSS transitions or animations applied to them, because they might conflict with Muuri's internal animation engine. However, the container element can have transitions applied to it if you want it to animate when it's size changes after the layout operation.
  • You can control the gaps between the items by giving some margin to the item elements.
  • One last thing: never ever set overflow: auto; or overflow: scroll; to the container element. Muuri's calculation logic does not account for that and you will see some item jumps when dragging starts. Always use a wrapper element for the container where you set the auto/scroll overflow values.
.grid {   position: relative; } .item {   display: block;   position: absolute;   width: 100px;   height: 100px;   margin: 5px;   z-index: 1;   background: #000;   color: #fff; } .item.muuri-item-dragging {   z-index: 3; } .item.muuri-item-releasing {   z-index: 2; } .item.muuri-item-hidden {   z-index: 0; } .item-content {   position: relative;   width: 100%;   height: 100%; }

6. Fire it up

The bare minimum configuration is demonstrated below. You must always provide the container element (or a selector so Muuri can fetch the element for you), everything else is optional.

var grid = new Muuri('.grid');

You can view this little tutorial demo here. After that you might want to check some other demos as well.

API

Grid constructor

Muuri is a constructor function and should be always instantiated with the new keyword. For the sake of clarity, we refer to a Muuri instance as grid throughout the documentation.

Syntax

Muuri( element, [options] )

Parameters

  • element  —  element / string
    • Default value: null.
    • You can provide the element directly or use a selector (string) which uses querySelector() internally.
  • options  —  object

Default options

The default options are stored in Muuri.defaultOptions object, which in it's default state contains the following configuration:

{    // Item elements   items: '*',    // Default show animation   showDuration: 300,   showEasing: 'ease',    // Default hide animation   hideDuration: 300,   hideEasing: 'ease',    // Item's visible/hidden state styles   visibleStyles: {     opacity: '1',     transform: 'scale(1)'   },   hiddenStyles: {     opacity: '0',     transform: 'scale(0.5)'   },    // Layout   layout: {     fillGaps: false,     horizontal: false,     alignRight: false,     alignBottom: false,     rounding: true   },   layoutOnResize: 100,   layoutOnInit: true,   layoutDuration: 300,   layoutEasing: 'ease',    // Sorting   sortData: null,    // Drag & Drop   dragEnabled: false,   dragContainer: null,   dragStartPredicate: {     distance: 0,     delay: 0,     handle: false   },   dragAxis: null,   dragSort: true,   dragSortInterval: 100,   dragSortPredicate: {     threshold: 50,     action: 'move'   },   dragReleaseDuration: 300,   dragReleaseEasing: 'ease',   dragHammerSettings: {     touchAction: 'none'   },    // Classnames   containerClass: 'muuri',   itemClass: 'muuri-item',   itemVisibleClass: 'muuri-item-shown',   itemHiddenClass: 'muuri-item-hidden',   itemPositioningClass: 'muuri-item-positioning',   itemDraggingClass: 'muuri-item-dragging',   itemReleasingClass: 'muuri-item-releasing'  }

You can modify the default options easily:

Muuri.defaultOptions.showDuration = 400; Muuri.defaultOptions.dragSortPredicate.action = 'swap';

This is how you would use the options:

// Minimum configuration. var gridA = new Muuri('.grid-a');  // Providing some options. var gridB = new Muuri('.grid-b', {   items: '.item' });

Grid options

items  

The initial item elements, which should be children of the container element. All elements that are not children of the container will be appended to the container. You can provide an array of elements, a node list or a selector (string). If you provide a selector Muuri uses it to filter the current child elements of the container element and sets them as initial items. By default all current child elements of the provided container element are used as initial items.

  • Default value: '*'.
  • Accepted types: array (of elements), node list, string, null.
// Use specific items. var grid = new Muuri(elem, {   items: [elemA, elemB, elemC] });  // Use node list. var grid = new Muuri(elem, {   items: elem.querySelectorAll('.item') });  // Use selector. var grid = new Muuri(elem, {   items: '.item' });

showDuration  

Show animation duration in milliseconds. Set to 0 to disable show animation.

  • Default value: 300.
  • Accepted types: number.
var grid = new Muuri(elem, {   showDuration: 600 });

showEasing  

Show animation easing. Accepts any valid Animation easing value.

  • Default value: 'ease'.
  • Accepted types: string.
var grid = new Muuri(elem, {   showEasing: 'cubic-bezier(0.215, 0.61, 0.355, 1)' });

hideDuration  

Hide animation duration in milliseconds. Set to 0 to disable hide animation.

  • Default value: 300.
  • Accepted types: number.
var grid = new Muuri(elem, {   hideDuration: 600 });

hideEasing  

Hide animation easing. Accepts any valid Animation easing value.

  • Default value: 'ease'.
  • Accepted types: string.
var grid = new Muuri(elem, {   hideEasing: 'cubic-bezier(0.215, 0.61, 0.355, 1)' });

visibleStyles  

The styles that will be applied to all visible items. These styles are also used for the show/hide animations which means that you have to have the same style properties in visibleStyles and hiddenStyles options. Be sure to define all style properties camel cased.

  • Default value: {opacity: 1, transform: 'scale(1)'}.
  • Accepted types: object.
var grid = new Muuri(elem, {   visibleStyles: {     opacity: 1,     transform: 'rotate(45deg)'   },   hiddenStyles: {     opacity: 0,     transform: 'rotate(-45deg)'   } });

hiddenStyles  

The styles that will be applied to all hidden items. These styles are also used for the show/hide animations which means that you have to have the same style properties in visibleStyles and hiddenStyles options. Be sure to define all style properties camel cased.

  • Default value: {opacity: 0, transform: 'scale(0.5)'}.
  • Accepted types: object.
var grid = new Muuri(elem, {   visibleStyles: {     opacity: 1,     transform: 'rotate(45deg)'   },   hiddenStyles: {     opacity: 0,     transform: 'rotate(-45deg)'   } });

layout  

Define how the items will be laid out. Muuri ships with a configurable layout algorithm which is used by default. It's pretty flexible and suitable for most common situations (lists, grids and even bin packed grids). If that does not fit the bill you can always provide your own layout algorithm (it's not as scary as it sounds).

  • Default value: {fillGaps: false, horizontal: false, alignRight: false, alignBottom: false}.
  • Accepted types: function, object.

Provide an object to configure the default layout algorithm with the following properties

  • fillGaps  —  boolean
    • Default value: false.
    • When true the algorithm goes through every item in order and places each item to the first available free slot, even if the slot happens to be visually before the previous element's slot. Practically this means that the items might not end up visually in order, but there will be less gaps in the grid. By default this option is false which basically means that the following condition will be always true when calculating the layout (assuming alignRight and alignBottom are false): nextItem.top > prevItem.top || (nextItem.top === prevItem.top && nextItem.left > prevItem.left). This also means that the items will be visually in order.
  • horizontal  —  boolean
    • Default value: false.
    • When true the grid works in landscape mode (grid expands to the right). Use for horizontally scrolling sites. When false the grid works in "portrait" mode and expands downwards.
  • alignRight  —  boolean
    • Default value: false.
    • When true the items are aligned from right to left.
  • alignBottom  —  boolean
    • Default value: false.
    • When true the items are aligned from the bottom up.
  • rounding  —  boolean
    • Default value: true.
    • When true the dimensions of the items will be automatically rounded for the layout calculations using Math.round(). Set to false to use accurate dimensions. In practice you would want disable this if you are using relative dimension values for items (%, em, rem, etc.). If you have defined item dimensions with pixel values (px) it is recommended that you leave this on.
// Customize the default layout algorithm. var grid = new Muuri(elem, {   layout: {     fillGaps: true,     horizontal: true,     alignRight: true,     alignBottom: true,     rounding: false   } });

Provide a function to use a custom layout algorithm

When you provide a custom layout function Muuri calls it whenever calculation of layout is necessary. Before calling the layout function Muuri always calculates the current width and height of the grid's container element and also creates an array of all the items that are part of the layout currently (all active items).

  • customLayout( items, gridWidth, gridHeight )
    • items  —  array
      • Array of Muuri.Item instances.
    • gridWidth  —  number
      • Current width of the grid's container element.
    • gridHeight  —  number
      • Current height of the grid's container element.

The layout function's job is using this data, which is provided to the layout function as arguments (as detailed above), and calculating position for each item in the array.

The layout function should always return an object with following properties:

  • slots  —  array
    • Array of the item positions (numbers). E.g. if the items were [a, b] this should be [aLeft, aTop, bLeft, bTop]. You have to calculate the left and top position for each item in the provided items array in the same order the items are provided.
  • width  —  number
    • The width of the grid.
  • height  —  number
    • The height of the grid.
  • setWidth  —  boolean
    • Should Muuri set the provided width as the grid element's width?
  • setHeight  —  boolean
    • Should Muuri set the provided height as the grid element's height?
// Build your own layout algorithm. var grid = new Muuri(elem, {   layout: function (items, gridWidth, gridHeight) {     // The layout data object. Muuri will read this data and position the items     // based on it.     var layout = {       // The layout's item slots (left/top coordinates).       slots: [],       // The layout's total width.       width: 0,       // The layout's total height.       height: 0,       // Should Muuri set the grid's width after layout?       setWidth: true,       // Should Muuri set the grid's height after layout?       setHeight: true     };      // Calculate the slots.     var item;     var m;     var x = 0;     var y = 0;     var w = 0;     var h = 0;     for (var i = 0; i < items.length; i++) {       item = items[i];       x += w;       y += h;       m = item.getMargin();       w = item.getWidth() + m.left + m.right;       h = item.getHeight() + m.top + m.bottom;       layout.slots.push(x, y);     }      // Calculate the layout's total width and height.      layout.width = x + w;     layout.height = y + h;      return layout;   } });

layoutOnResize  

Should Muuri automatically trigger layout method on window resize? Set to false to disable. When a number or true is provided Muuri will automatically lay out the items every time window is resized. The provided number (true is transformed to 0) equals to the amount of time (in milliseconds) that is waited before items are laid out after each window resize event.

  • Default value: 100.
  • Accepted types: boolean, number.
// No layout on resize. var grid = new Muuri(elem, {   layoutOnResize: false });
// Layout on resize (instantly). var grid = new Muuri(elem, {   layoutOnResize: true });
// Layout on resize (with 200ms debounce). var grid = new Muuri(elem, {   layoutOnResize: 200 });

layoutOnInit  

Should Muuri trigger layout method automatically on init?

  • Default value: true.
  • Accepted types: boolean.
var grid = new Muuri(elem, {   layoutOnInit: false });

layoutDuration  

The duration for item's layout animation in milliseconds. Set to 0 to disable.

  • Default value: 300.
  • Accepted types: number.
var grid = new Muuri(elem, {   layoutDuration: 600 });

layoutEasing  

The easing for item's layout animation. Accepts any valid Animation easing value.

  • Default value: 'ease'.
  • Accepted types: string.
var grid = new Muuri(elem, {   layoutEasing: 'cubic-bezier(0.215, 0.61, 0.355, 1)' });

sortData  

The sort data getter functions. Provide an object where the key is the name of the sortable attribute and the function returns a value (from the item) by which the items can be sorted.

  • Default value: null.
  • Accepted types: object, null.
var grid = new Muuri(elem, {   sortData: {     foo: function (item, element) {       return parseFloat(element.getAttribute('data-foo'));     },     bar: function (item, element) {       return element.getAttribute('data-bar').toUpperCase();     }   } }); // Refresh sort data whenever an item's data-foo or data-bar changes grid.refreshSortData(); // Sort the grid by foo and bar. grid.sort('foo bar');

dragEnabled  

Should items be draggable?

  • Default value: false.
  • Accepted types: boolean.
var grid = new Muuri(elem, {   dragEnabled: true });

dragContainer  

The element the dragged item should be appended to for the duration of the drag. If set to null (which is also the default value) the grid's container element will be used.

  • Default value: null.
  • Accepted types: element, null.
var grid = new Muuri(elem, {   dragContainer: document.body });

dragStartPredicate  

A function that determines when the item should start to move when the item is being dragged. By default uses the built-in predicate which has some configurable options.

  • Default value: {distance: 0, delay: 0, handle: false}.
  • Accepted types: function, object.

If an object is provided the default sort predicate handler will be used. You can define the following properties:

  • distance  —  number
    • Default value: 0.
    • How many pixels must be dragged before the dragging starts.
  • delay  —  number
    • Default value: 0.
    • How long (in milliseconds) the user must drag before the dragging starts.
  • handle  —  string / boolean
    • Default value: false.
    • The selector(s) which much match the event target element for the dragging to start. Note that if the event target element is a descendant of the handle element(s) it is also considered a match, which should be pretty useful in most scenarios.

If you provide a function you can totally customize the drag start logic. When the user starts to drag an item this predicate function will be called until you return true or false. If you return true the item will begin to move whenever the item is dragged. If you return false the item will not be moved at all. Note that after you have returned true or false this function will not be called until the item is released and dragged again.

The predicate function receives two arguments:

  • item  —  Muuri.Item
    • The item that's being dragged.
  • event  —  object
    • The drag event (Hammer.js event).
// Configure the default predicate var grid = new Muuri(elem, {   dragStartPredicate: {     distance: 10,     delay: 100,     handle: '.foo, .bar'   } });
// Provide your own predicate var grid = new Muuri(elem, {   dragStartPredicate: function (item, e) {     // Start moving the item after the item has been dragged for one second.     if (e.deltaTime > 1000) {       return true;     }   } });
// Pro tip: provide your own predicate and fall back to the default predicate. var grid = new Muuri(elem, {   dragStartPredicate: function (item, e) {     // If this is final event in the drag process, let's prepare the predicate     // for the next round (do some resetting/teardown). The default predicate     // always needs to be called during the final event i

Leave a Reply

If you leave a message for the first time on this site, the audit will need to display the contents!