Shuffle.js: Responsive Sortable Filterable Grid Layout Library

File Size: 38.6 KB
Views Total: 35708
Last Update:
Publish Date:
Official Website: Go to website
License: MIT
   
Shuffle.js: Responsive Sortable Filterable Grid Layout Library

Shuffle.js is a JavaScript library that adds filter, sort, and category grouping to a responsive grid of HTML items. 

Wrap a set of elements into a container, tag each one with group names, and Shuffle handles the rest. 

It keeps the grid fluid on window resize, recalculates positions, and applies smooth CSS transitions when items change visibility or order. 

The library works with any CSS layout system, including Bootstrap’s row/col grid, and gives you full control over which items appear and in what sequence.

Note that the plugin now works as a Vanilla JavaScript plugin since 4.0.

Features:

  • Filters grid items by category groups.
  • Sorts cards by custom values, random order, or reverse order.
  • Rearranges responsive columns after filters, sorts, and resize changes.
  • Animates layout changes through CSS transitions.
  • Works with Bootstrap column grids.
  • Handles dynamic item insertion and removal.
  • Supports centered layouts and RTL layouts.
  • Adds visibility states for filtered items.

How to use it:

1. Install Shuffle.js from npm when your project uses Vite, Rollup, Webpack, Parcel, or another ESM-capable build setup.

# NPM
$ npm install shufflejs --save
import Shuffle from 'shufflejs';

2. For a quick browser demo, import Shuffle.js as an ES module from a CDN.

<script type="module">
  import Shuffle from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';
</script>

3. The basic HTML structure to create a filterable grid. Each grid item needs a category source. The default setup reads data-groups as a valid JSON array of strings.

<div class="filter-toolbar" aria-label="Project filters">
  <button type="button" data-filter="all">All</button>
  <button type="button" data-filter="ui">UI Kits</button>
  <button type="button" data-filter="plugin">Plugins</button>
  <button type="button" data-filter="template">Templates</button>
</div>

<div id="resourceGrid" class="resource-grid">
  <article class="resource-card" data-groups='["ui","template"]' data-title="Admin Control Panel">
    <img src="admin-panel.jpg" alt="Admin dashboard preview">
    <h3>Admin Control Panel</h3>
    <p>Dashboard layout for SaaS admin pages.</p>
  </article>

  <article class="resource-card" data-groups='["plugin"]' data-title="Image Compare Slider">
    <img src="image-compare.jpg" alt="Image comparison UI">
    <h3>Image Compare Slider</h3>
    <p>Before and after image comparison widget.</p>
  </article>

  <article class="resource-card" data-groups='["ui"]' data-title="Pricing Card Set">
    <img src="pricing-cards.jpg" alt="Pricing card layout">
    <h3>Pricing Card Set</h3>
    <p>Responsive pricing UI for landing pages.</p>
  </article>

  <div class="resource-sizer"></div>
</div>

4. Add your own item sizing CSS. Shuffle.js handles positioning, but your project controls the grid item width.

.resource-grid {
  position: relative;
  overflow: hidden;
}

.resource-card,
.resource-sizer {
  width: 25%;
}

.resource-card {
  padding: 12px;
}

.resource-card img {
  display: block;
  width: 100%;
  height: auto;
}

@media (max-width: 900px) {
  .resource-card,
  .resource-sizer {
    width: 50%;
  }
}

@media (max-width: 560px) {
  .resource-card,
  .resource-sizer {
    width: 100%;
  }
}

5. Attach the plugin to the top container and specify the selector of grid items.

const grid = document.getElementById('resourceGrid');

const shuffle = new Shuffle(grid, {
  itemSelector: '.resource-card',
  sizer: '.resource-sizer'
});

document.querySelectorAll('[data-filter]').forEach((button) => {
  button.addEventListener('click', () => {
    const group = button.dataset.filter;
    shuffle.filter(group === 'all' ? Shuffle.ALL_ITEMS : group);
  });
});

6. All possible options to customize the grid layout.

/**
 * Useful for percentage based heights when they might not always be exactly
 * the same (in pixels).
 */
buffer?: number;

/**
 * Reading the width of elements isn't precise enough and can cause columns to
 * jump between values.
 */
columnThreshold?: number;

/**
 * A static number or function that returns a number which determines
 * how wide the columns are (in pixels).
 */
columnWidth?: number | ((containerWidth: number) => number);

/**
 * If your group is not json, and is comma delimited, you could set delimiter to ','.
 */
delimiter?: string | null;

/**
 * CSS easing function to use.
 */
easing?: string;

/**
 * Affects using an array with filter. e.g. `filter(['one', 'two'])`. With "any",
 * the element passes the test if any of its groups are in the array. With "all",
 * the element only passes if all groups are in the array.
 */
filterMode?: FilterModeOptions;

/**
 * Initial filter group.
 */
group?: string;

/**
 * A static number or function that determines how wide the gutters
 * between columns are (in pixels).
 */
gutterWidth?: number | ((containerWidth: number) => number);

/**
 * Shuffle can be initialized with a sort object. It is the same object
 * given to the sort method.
 * Sort Options:
 * by (Function, default `null`): Receives the selected item property and returns the value used for sorting.
 * compare (Function, default `null`): Receives two `ShuffleItem` objects and returns a custom sort result.
 * key (String, default `'element'`): Selects the `ShuffleItem` property passed into `by`.
 * randomize (Boolean, default `false`): Randomizes the current item order.
 * reverse (Boolean, default `false`): Reverses the final sorted array.
 */
initialSort?: SortOptions | null;

/**
 * Whether to center grid items in the row with the leftover space.
 */
isCentered?: boolean;

/**
 * Whether to align grid items to the right in the row.
 */
isRTL?: boolean;

/**
 * e.g. '.picture-item'.
 */
itemSelector?: string;

/**
 * Whether to round pixel values used in translate(x, y). This usually avoids blurriness.
 */
roundTransforms?: boolean;

/**
 * Element or selector string. Use an element to determine the size of columns and gutters.
 */
sizer?: ElementOption | null;

/**
 * Transition/animation speed (milliseconds).
 */
speed?: number;

/**
 * Transition delay offset for each item in milliseconds.
 */
staggerAmount?: number;

/**
 * Maximum stagger delay in milliseconds.
 */
staggerAmountMax?: number;

/**
 * Whether to use transforms or absolute positioning.
 */
useTransforms?: boolean;

7. API methods.

// Filter all items by one group.
shuffle.filter('template');

// Filter items by several groups.
// The result depends on the filterMode option.
shuffle.filter(['ui', 'template']);

// Filter items with custom logic.
shuffle.filter((element) => {
  return element.dataset.featured === 'true';
});

// Filter and then sort the visible results.
shuffle.filter('plugin', {
  by: (element) => element.dataset.title.toLowerCase()
});

// Sort the currently visible items.
shuffle.sort({
  by: (element) => Number(element.dataset.price)
});

// Reset sorted items to DOM order.
shuffle.sort({});

// Recalculate sizes and reposition items.
shuffle.update({
  recalculateSizes: true,
  force: false
});

// Reposition items when sizes changed but columns and gutters did not.
shuffle.layout();

// Register new elements after appending them to the grid.
const newItems = Array.from(document.querySelectorAll('.resource-card.is-new'));
shuffle.add(newItems);

// Stop automatic resize updates.
shuffle.disable();

// Re-enable Shuffle and recalculate the layout.
shuffle.enable(true);

// Re-enable Shuffle and skip the layout update.
shuffle.enable(false);

// Remove one or more grid items.
const removedItems = Array.from(document.querySelectorAll('.resource-card.is-archived'));
shuffle.remove(removedItems);

// Get the ShuffleItem object for a DOM element.
const item = shuffle.getItemByElement(document.querySelector('.resource-card'));

// Destroy the instance and remove Shuffle styles, classes, events, and references.
shuffle.destroy();

8. Events.

// Run code after Shuffle finishes a layout pass.
shuffle.on(Shuffle.EventType.LAYOUT, () => {
  console.log('Grid layout finished.');
});

// Read removed elements after Shuffle removes items.
shuffle.on(Shuffle.EventType.REMOVED, (data) => {
  console.log(data.collection);
  console.log(data.shuffle);
});

10. Shuffle.js applies internal styles to each item during initialization, visibility changes, hidden states, and direction changes. You can adjust those defaults before the instance runs.

Shuffle.ShuffleItem.Css.INITIAL = {
  position: 'absolute',
  top: 0,
  visibility: 'visible',
  willChange: 'transform'
};

Shuffle.ShuffleItem.Css.DIRECTION = {
  ltr: {
    left: 0
  },
  rtl: {
    right: 0
  }
};

Shuffle.ShuffleItem.Css.VISIBLE = {
  before: {
    opacity: 1,
    visibility: 'visible'
  },
  after: {
    transitionDelay: ''
  }
};

Shuffle.ShuffleItem.Css.HIDDEN = {
  before: {
    opacity: 0
  },after: {
    visibility: 'hidden',
    transitionDelay: ''
  }
};

Shuffle.ShuffleItem.Scale = {
  VISIBLE: 1,
  HIDDEN: 0.001
};

// Change these values before you create the Shuffle instance.

// Add an initial visual marker to every managed item.
Shuffle.ShuffleItem.Css.INITIAL.backgroundColor = 'rgba(0, 128, 128, 0.08)';

// Change the hidden-item scale.
Shuffle.ShuffleItem.Scale.HIDDEN = 0.5;

Advanced Examples:

Example 1: Filter A Resource Directory With A Search Form

Use a function filter when category buttons alone do not cover the search pattern. This example combines a text search field and a category select.

<form id="resourceSearch" class="resource-search">
  <input type="search" id="keywordInput" placeholder="Search resources">
  <select id="typeSelect">
    <option value="all">All types</option>
    <option value="ui">UI Kits</option>
    <option value="plugin">Plugins</option>
    <option value="template">Templates</option>
  </select>
</form>

<div id="directoryGrid" class="resource-grid">
  <article class="resource-card" data-title="Calendar Booking UI" data-groups='["ui","template"]'>
    <h3>Calendar Booking UI</h3>
  </article>

  <article class="resource-card" data-title="Lightbox Gallery Plugin" data-groups='["plugin"]'>
    <h3>Lightbox Gallery Plugin</h3>
  </article>

  <article class="resource-card" data-title="Pricing Section Template" data-groups='["template"]'>
    <h3>Pricing Section Template</h3>
  </article>

  <div class="resource-sizer"></div>
</div>

<script type="module">
  import Shuffle from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';

  const grid = document.getElementById('directoryGrid');
  const keywordInput = document.getElementById('keywordInput');
  const typeSelect = document.getElementById('typeSelect');

  const directory = new Shuffle(grid, {
    itemSelector: '.resource-card',
    sizer: '.resource-sizer'
  });

  function applyDirectoryFilter() {
    const keyword = keywordInput.value.trim().toLowerCase();
    const selectedType = typeSelect.value;

    directory.filter((element) => {
      const title = element.dataset.title.toLowerCase();
      const groups = JSON.parse(element.dataset.groups);

      const keywordMatch = keyword === '' || title.includes(keyword);
      const typeMatch = selectedType === 'all' || groups.includes(selectedType);

      return keywordMatch && typeMatch;
    });
  }

  keywordInput.addEventListener('input', applyDirectoryFilter);
  typeSelect.addEventListener('change', applyDirectoryFilter);
</script>

Example 2: Sort Product Cards By Price Or Name

Use sort() when the grid needs a user-controlled order. Store sortable values in data-* attributes.

<div class="sort-toolbar">
  <button type="button" data-sort="name">Name</button>
  <button type="button" data-sort="price-low">Price Low</button>
  <button type="button" data-sort="price-high">Price High</button>
</div>

<div id="productGrid" class="resource-grid">
  <article class="product-card" data-groups='["software"]' data-name="Icon Pack" data-price="19">
    <h3>Icon Pack</h3>
    <p>$19</p>
  </article>

  <article class="product-card" data-groups='["template"]' data-name="Landing Page Kit" data-price="49">
    <h3>Landing Page Kit</h3>
    <p>$49</p>
  </article>

  <article class="product-card" data-groups='["plugin"]' data-name="Chart Widget" data-price="29">
    <h3>Chart Widget</h3>
    <p>$29</p>
  </article>

  <div class="product-sizer"></div>
</div>

<script type="module">
  import Shuffle from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';

  const products = new Shuffle(document.getElementById('productGrid'), {
    itemSelector: '.product-card',
    sizer: '.product-sizer'
  });

  document.querySelectorAll('[data-sort]').forEach((button) => {
    button.addEventListener('click', () => {
      const sortType = button.dataset.sort;

      if (sortType === 'name') {
        products.sort({
          by: (element) => element.dataset.name.toLowerCase()
        });
      }

      if (sortType === 'price-low') {
        products.sort({
          by: (element) => Number(element.dataset.price)
        });
      }

      if (sortType === 'price-high') {
        products.sort({
          by: (element) => Number(element.dataset.price),
          reverse: true
        });
      }
    });
  });
</script>

Example 3: Add AJAX Loaded Cards To An Existing Grid

Use add() after your code appends new elements to the Shuffle container. The method registers the new nodes and adds them to the layout.

<button type="button" id="loadMoreResources">Load more resources</button>

<div id="ajaxGrid" class="resource-grid">
  <article class="ajax-card" data-groups='["template"]'>
    <h3>Starter Dashboard</h3>
  </article>

  <div class="ajax-sizer"></div>
</div>

<template id="resourceCardTemplate">
  <article class="ajax-card">
    <img alt="">
    <h3></h3>
    <p></p>
  </article>
</template>

<script type="module">
  import Shuffle from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';

  const ajaxGrid = document.getElementById('ajaxGrid');
  const cardTemplate = document.getElementById('resourceCardTemplate');
  const loadButton = document.getElementById('loadMoreResources');

  const resourceGrid = new Shuffle(ajaxGrid, {
    itemSelector: '.ajax-card',
    sizer: '.ajax-sizer'
  });

  loadButton.addEventListener('click', async () => {
    const response = await fetch('/data/resources.json');
    const resources = await response.json();

    const newCards = resources.map((resource) => {
      const fragment = cardTemplate.content.cloneNode(true);
      const card = fragment.querySelector('.ajax-card');

      card.dataset.groups = JSON.stringify(resource.groups);
      card.querySelector('img').src = resource.image;
      card.querySelector('img').alt = resource.alt;
      card.querySelector('h3').textContent = resource.title;
      card.querySelector('p').textContent = resource.summary;

      ajaxGrid.insertBefore(card, ajaxGrid.querySelector('.ajax-sizer'));

      return card;
    });

    resourceGrid.add(newCards);
  });
</script>

Example 4: Use Shuffle.js With Bootstrap 4 Columns

Bootstrap 4 uses flexbox rows and column classes. Set the Shuffle container on the .row, then make each managed item a column.

<div class="row" id="bootstrapGallery">
  <figure class="col-6 col-md-4 col-lg-3 gallery-item" data-groups='["dashboard"]'>
    <img src="dashboard-card.jpg" class="img-fluid" alt="Dashboard UI preview">
    <figcaption>Dashboard UI</figcaption>
  </figure>

  <figure class="col-6 col-md-4 col-lg-3 gallery-item" data-groups='["form"]'>
    <img src="checkout-form.jpg" class="img-fluid" alt="Checkout form preview">
    <figcaption>Checkout Form</figcaption>
  </figure>

  <figure class="col-6 col-md-4 col-lg-3 gallery-item" data-groups='["gallery"]'>
    <img src="gallery-layout.jpg" class="img-fluid" alt="Gallery layout preview">
    <figcaption>Gallery Layout</figcaption>
  </figure>

  <div class="col-6 col-md-4 col-lg-3 gallery-sizer"></div>
</div>

<script type="module">
  import Shuffle from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';

  const bootstrapGallery = new Shuffle(document.getElementById('bootstrapGallery'), {
    itemSelector: '.gallery-item',
    sizer: '.gallery-sizer'
  });
</script>

Alternatives and Related Resources

  • Isotope: Use this jQuery layout plugin for richer layout modes, filtering, and sorting.
  • Quicksand: Use this jQuery plugin for reorder and filter effects with animated item changes.
  • Filterable jQuery Gallery: Use this gallery plugin for categorized photo grids with a basic shuffle animation.
  • TJ gallery: Use this jQuery gallery plugin for responsive justified image layouts.
  • 10 Best Grid Layout Systems In JavaScript And CSS: Use this roundup when you need to compare masonry, grid, and dynamic layout libraries.

FAQs:

Q: Why do my grid items overlap after page load?
A: Image-dependent heights often cause overlap when the instance runs before images finish loading. Initialize after image loading or call shuffle.layout() after image dimensions settle.

Q: Can I use comma-separated groups instead of JSON arrays?
A: Yes. Set delimiter: ',' and write markup such as data-groups="ui,template". The default setup expects a valid JSON array.

Q: Why does the grid leave empty spaces?
A: Shuffle’s placement algorithm does not backfill every empty gap. Use Packery when your layout requires gap filling.

Q: Can I use Shuffle with Bootstrap 5 without the sizer element?
A: Yes, but using the sizer gives more reliable column width detection with percentage‑based column classes. Drop a <div class="col-1 js-shuffle-sizer"></div> as the last child of .row and pass it as the sizer option. If you omit the sizer, Shuffle uses the width of the first item, which can be inaccurate when columns are defined by CSS classes.

Q: How do I center the grid items?
A: Set isCentered: true in the options. The library will calculate left‑over horizontal space and translate each row’s items by half that amount. This works with both useTransforms true and false.

Q: What is the difference between update() and layout()?
A: update() recalculates column and gutter sizes (e.g., after a container width change) and then re‑positions items. layout() only re‑positions items using the existing column count. Use update() on window resize or after content changes that affect sizing; use layout() when only item visibility or order changes.

Q: Can I animate items with a custom CSS transition?
A: Yes. Shuffle sets transition-property, transition-duration, and transition-timing-function directly on each item. You can override the easing with the easing option. For more complex animations, override ShuffleItem.Css static properties to inject custom styles for the visible/hidden states.

Changelog:

v7.0.0 (2026-06-12)

  • Major update

v6.1.1 (2024-02-23)

  • Bugfixes

v6.1.0 (2022-07-07)

  • The package.json now contains sideEffects: false to improve dead code removal and tree shaking.
  • The package.json now contains an export-map to assist bundlers in choosing the correct file.
  • Bug fixes

v6 (2022-02-15)

  • Remove IE 11 from browsers list. If you need to support IE 11 (sorry), please use v5. Did you know Microsoft 365 apps and services stopped supporting IE 11 in August 2021?
  • Remove matches-selector package and use the native matches (see browser support).
  • Remove deprecated delimeter option (the misspelled one). Use the delimiter option instead.
  • Replace window resize event listener with ResizeObserver (#321). Browser support for it is very good, but if you want to support a browser that doesn't have it, you can manually add a window resize event and call update() within the event callback.
  • Changed the method signature for update().
  • Changed how data attribute are accessed. Previously, Shuffle used element.getAttribute('data-groups'). Now, it uses element.dataset.groups. dataset is very well supported now.
  • Added force option to update method to force shuffle to update even if it's disabled
  • Convert demos to ES6 classes.
  • Move browsers list to .browserslistrc.
  • Add prettier
  • Add BMC button.

v5.4.1 (2021-06-01)

  • Add sortedItems property which is the shuffle items in sorted order. Only visible items are in this array.
  • Fix Chrome DPI bug that was causing column sizes to be calculated incorrectly.

v5.3.0 (2021-03-23)

  • Add isRTL option

v5.2.3 (2019-11-26)

  • Major update
  • Doc updated

v4.0.0 (2016-04-20)

  • Use ES6 export for main file. Add index.js to export the `default` to module.exports. This would allow module bundlers like rollup to use jsnext:main and it'll all be ES6 import/exports

v3.1.1 (2015-03-25)

  • Update

v3.0.4 (2015-02-17)

  • Add NPM support

v3.0.1 (2014-12-30)

  • Add CommonJS support

v3.0.0 (2014-11-27)

  • Add blurred image for caption.

v2.1.2 (2014-06-02)

  • update.

v2.1.0 (2014-04-19)

  • update.

v2.1.0 (2014-04-13)

  • update.

v2.0.8 (2014-04-12)

  • Add more demos.
  • Add AMD support. 

v2.0.6 (2014-03-16)

  • bug fixed.

v2.0.5 (2014-03-09)

  • Add bootstrap 3 demo, fix percentage width issue with Shuffle

This awesome jQuery plugin is developed by glen-cheney. For more Advanced Usages, please check the demo page or visit the official website.