This tutorial will review how to apply multiple filters to an isotope container using the IsotopeJS library‘s features in Bricks.
Table of Contents
Requirements
Custom fields
For this tutorial, I created a Custom type called Tutorials (but feel free to choose whatever you want) and attached three customs fields to it: Type, Estimated Read, and Difficulty.

Then, in bricks, I built my isotope container using the native query loop element and added all my elements to it. My isotope container looks like this:

Of course, this is just an example to illustrate the structure needed to create a correct isotope container – feel free to add any element that you want inside your query loop.
Add custom inputs
The whole idea is to insert some custom HTML inputs (such as checkboxes, radio buttons, or a range slider) and execute the filter function once any of the inputs get triggered.
So we’ll basically need 3 elements for each filter:
- the HTML code inside bricks to create the input
- a JavaScript code ( an EventListener) that will trigger the filter function
- the filter function that will return the filtered items if the criteria are met.
Let’s review the most used HTML inputs one by one.
Input buttons
The buttons have been covered in PART 1 and PART 2 so I won’t spend too much time on how to set them inside Bricks. Let’s directly jump into our JavaScript code.
First of all, we need to save all the buttons inside a variable:
var buttons = wrapper.querySelectorAll(".filterbtn-container .filterbtn");
// Show a message in console if no buttons have been found
if (buttons.length < 1) {
console.log('No filter wrapper or filter buttons found. Make sure your filter wrapper has the class ".filterbtn-wrapper" and all your filter buttons have the class ".filterbtn"');
}
And create a new EventListener in order to trigger the filter every time a button is clicked:
// Event Listener for buttons
if (buttons.length > 0) {
buttons.forEach(elem => elem.addEventListener("click", (event) => {
event.preventDefault();
// get the data-filter attribute from the filter button
var filterValue = event.target.getAttribute("data-filter");
filterSelector = filterValue;
// filter results
iso.arrange();
}));
};
Now let’s create the filter function inside the isotope options:
// Default variable
var filterRes = true;
var filterSelector = "*";
// init Isotope
var isotopeOptions = {
itemSelector: '.isotope-selector',
layoutMode: isotopeLayoutHelper,
filter: (itemElem1, itemElem2) => {
const itemElem = itemElem1 || itemElem2;
// tags/buttons
if (buttons.length > 0) {
filterRes = filterSelector != '*' ? itemElem.dataset.filter.includes(filterSelector) : true;
}
return filterRes;
}
};
A bit of explanation:
- we set a default
filterResvariable totrue, so even if there are no filter buttons on your page, the filter function will returntrueand the other filters will still work as expected. - we set a default
filterSelectorvariable to*in order to avoid any filtering on load. - in the
isotopeOptionsvariable, we added a newfilterobject that will take care of the filtering logic. - The main filter condition is: if the
filterSelectoris set to*, it will returntruefor all the items – thus no filtering will be applied, otherwise it will filter the items that contain the samedata-filtervalue than the button we just clicked.
Input text
Let’s create a quick search input field that will filter our isotope selectors based on the keywords inserted in the text field.
First of all, let’s add a new code element inside our isotope-wrapper and paste this HTML code in it:
<fieldset>
<legend>Search:</legend>
<input type="text" id="quicksearch" placeholder="Type a keyword" />
</fieldset>

As you can see, we set the ID of the text input to quicksearch in order to be easily queryable by JavaScript. Let’s assign that input field inside a variable called quickSearch:
var quickSearch = wrapper.querySelector('#quicksearch');
// Show a message in console if no search input have been found
if (!quickSearch) {
console.log('No QuickSearch found. Make sure your search input has the ID "#quicksearch"');
}
The next step is to build our Event Listener to trigger the filter function that we’re going to create in a moment.
// Event Listener for search input
if (quickSearch) {
quickSearch.addEventListener('keyup', debounce((event) => {
qsRegex = new RegExp(quickSearch.value, 'gi');
//filter the store list
iso.arrange();
}, 200));
}
Our trigger is listening for any input from the keyboard and registering the value (which is the typed text) of our field inside the qsRegex variable that will later be processed by our filter function.
Note that we are using a debounce function to avoid triggering the filter every single time we type a letter – but only after a delay of 200ms. Here is the debounce function:
// debounce so filtering doesn't happen every millisecond
const debounce = (fn, threshold) => {
var timeout;
threshold = threshold || 200;
return function debounced() {
clearTimeout(timeout);
var args = arguments;
var _this = this;
function delayed() {
fn.apply(_this, args);
}
timeout = setTimeout(delayed, threshold);
};
};
The final step is to create the filter function inside the isotopeOptions variable:
// Default variable
var filterSearch = true;
var qsRegex;
// init Isotope
var isotopeOptions = {
itemSelector: '.isotope-selector',
layoutMode: isotopeLayoutHelper,
filter: (itemElem1, itemElem2) => {
const itemElem = itemElem1 || itemElem2;
// quicksearch filter function
if (quickSearch) {
filterSearch = qsRegex ? itemElem.textContent.match(qsRegex) : true;
}
return filterSearch;
}
};
Explanation:
- we set a default
filterSearchvariable totrue, so even if there is no quick search input on your page, the filter function will returntrueand the other filters will still work as expected. - we added a
filterobject inside theisotopeOptionsvariable that will take care of the filtering logic. - The main filter condition is: if the text typed inside the search field matches the text of the isotope selector, the function returns
trueand the isotope selector will be visible inside the isotope container. If the text doesn’t match the isotope selector content, the function returnsfalsethe and the isotope selector gets filtered.
Input range slider
Let’s replicate the same logic with an HTML slider input. But before that, we need to add a new data-attribute called data-range to our isotope selectors that will dynamically return the value of our custom field. In this example, I’m using the range slider to filter my selectors by the Estimated Read Time field we set earlier:

Now, let’s create our HTML slider inside the isotope-wrapper. Copy/paste the code inside a block element:
<fieldset>
<legend>Estimated Read:</legend>
<input type="range" id="range" min="0" max="30" value="30" oninput="this.nextElementSibling.value = this.value + ' minute(s)'">
<output>30 minutes</output>
</fieldset>

Note that I added a custom JavaScript function to add the value of the slider and the units to the output. This is totally optional, feel free to skip it if you don’t need it.
Let’s jump back to our JavaScript code and register our range element inside a variable through its ID:
var range = wrapper.querySelector('#range');
// Show a message in console if no range slider have been found
if (!range) {
console.log('No Range found. Make sure your range input has the ID "#range"');
}
Now we want to create an Event listener that will trigger the filter function every time the slider is being moved. Actually, we’ll create two different events: one when the slider is moved by the mouse and another one when it’s moved by the keyboard keys. In order to avoid repeating the same code two times, let’s create a function rangeFilterFN that we will use on both Event Listeners:
// Event Listener for range slider
if (range) {
const rangeFilterFN = (e) => {
filterRangeValue = parseInt(e.target.value);
//filter the store list
iso.arrange();
}
range.addEventListener('input', debounce((e) => {
rangeFilterFN(e);
}, 200));
range.addEventListener('keyup', debounce((e) => {
rangeFilterFN(e);
}, 200));
}
Since the value of the HTML slider is a string – and we want to apply some numerical logic to our filter function – we need to convert it to be a valid integer using the parseInt() function.
Once our Event Listeners are correctly set, we need to create the filter function inside our isotopeOptions variable. The idea is to check whether the data-attribute value of each selector is less or equal to the value of the slider that we are listening to:
// Default variable
var filterRange = true;
var filterRangeValue = '*';
// init Isotope
var isotopeOptions = {
itemSelector: '.isotope-selector',
layoutMode: isotopeLayoutHelper,
filter: (itemElem1, itemElem2) => {
const itemElem = itemElem1 || itemElem2;
// range
if (range) {
filterRange = filterRangeValue != '*' ? parseInt(itemElem.dataset.range) <= filterRangeValue : true;
}
return filterRange;
}
};
Again, the data-attribute value is a string, so we need to convert it to an integer in order to apply numerical conditions such as “less than”.
If the data-attribute value is less or equal to the slider value, the function will return a true value and the isotope selector will be visible inside the isotope container. If the data-attribute value is higher than the slider value, the function will return false and the selector will be filtered.
Input checkbox
Just like we did earlier with the input range, we need to add a data-attribute called data-checkbox to our isotope selectors:

The returned value for each selector will be the value of the Difficulty field we set earlier.
Let’s add a new code element inside our isotope-wrapper with the following HTML code:
<fieldset>
<legend>Difficulty:</legend>
<div class="checkbox-wrapper">
<input type="checkbox" id="easy" name="difficulty" value="easy" class="checkbox" checked>
<label for="easy">Easy</label>
</div>
<div class="checkbox-wrapper">
<input type="checkbox" id="medium" name="difficulty" value="medium" class="checkbox" checked>
<label for="medium">Medium</label>
</div>
<div class="checkbox-wrapper">
<input type="checkbox" id="hard" name="difficulty" value="hard" class="checkbox" checked>
<label for="hard">Hard</label>
</div>
</fieldset>

And register all the checkboxes inside a variable called checkboxes:
var checkboxes = wrapper.querySelectorAll('.checkbox');
// Show a message in console if no checkboxes have been found
if (checkboxes.length < 1) {
console.log('No checkbox found. Make sure your checkbox input has the class ".checkbox"');
}
Now the logic is slightly trickier, so bear with me! We need to listen to each checkbox for any click event and, once any of them gets clicked, we need to loop into each checkbox again and see which ones are checked and – if they are – store their value inside an array that we will use later in our filter function.
// Event Listener for checkboxes
if (checkboxes) {
checkboxes.forEach(checkbox => {
checkbox.addEventListener('click', debounce(() => {
arrCheckbox = [];
checkboxes.forEach(cb => {
if (!cb.checked) {
return;
}
arrCheckbox.push(cb.value);
})
filterCheckboxValue = arrCheckbox;
//filter the store list
iso.arrange();
}, 200));
})
}
Let’s now create the filter function. Here is the logic: we need to see if the data-attribute value of each selector is included in the list of the active checkboxes. This list is the array with all the values of the active checkboxes we created earlier inside our Event Listener function. So basically, we need to check if the data-attribute value is included inside our array:
// Default variable
var filterCheckbox = true;
var filterCheckboxValue = '*';
// init Isotope
var isotopeOptions = {
itemSelector: '.isotope-selector',
layoutMode: isotopeLayoutHelper,
filter: (itemElem1, itemElem2) => {
const itemElem = itemElem1 || itemElem2;
// checkboxes
if (checkboxes.length > 0) {
filterCheckbox = filterCheckboxValue != '*' ? filterCheckboxValue.includes(itemElem.dataset.checkbox) : true;
}
return filterCheckbox;
}
};
If the data-attribute value is included in the array, the function returns true and the selector will be visible inside the isotope container. If the data-attribute value is not included in the array, the function returns false and the selector will be filtered.
Input radio buttons
Let’s replicate the same logic that we applied earlier and create a data-attribute called data-radio to our isotope selectors:

This data-attribute will return the value of our Type ACF field that we set at the beginning.
Now let’s create a new code element inside our isotope-wrapper and paste the following HTML code:
<fieldset>
<legend>Type:</legend>
<div class="radio-wrapper">
<input type="radio" id="single" name="type" value="single" class="radio">
<label for="single">Single Article</label>
</div>
<div class="radio-wrapper">
<input type="radio" id="series" name="type" value="series" class="radio">
<label for="series">Series</label>
</div>
</fieldset>

Just like we did before, let’s assign all the radio buttons to a radios variable:
var radios = wrapper.querySelectorAll('.radio');
// Show a message in console if no radio buttons have been found
if (checkboxes.length < 1) {
console.log('No radio found. Make sure your radio input has the class ".radio"');
}
And let’s apply the exact same logic we used for checkboxes:
// Event Listener for radio buttons
if (radios) {
radios.forEach(radio => {
radio.addEventListener('change', debounce(() => {
var arrRadio = [];
radios.forEach(radio => {
if (!radio.checked) {
return;
}
arrRadio.push(radio.value);
})
filterRadioValue = arrRadio;
//filter the store list
iso.arrange();
}, 200));
})
}
The same goes for the filter function:
// Default variable
var filterRadio = true;
var filterRadioValue = '*';
// init Isotope
var isotopeOptions = {
itemSelector: '.isotope-selector',
layoutMode: isotopeLayoutHelper,
filter: (itemElem1, itemElem2) => {
const itemElem = itemElem1 || itemElem2;
// radio
if (radios.length > 0) {
filterRadio = filterRadioValue != '*' ? filterRadioValue.includes(itemElem.dataset.radio) : true;
}
return filterRadio;
}
};
And that’s it!
Final code
Here is the final DOM tree of our filters in Bricks:

And that’s final Javascript code that you can copy/paste inside your init.js file:
window.addEventListener('DOMContentLoaded', () => {
const isotopeWrappers = document.querySelectorAll('.isotope-wrapper');
// Stop the function if there is no isotope wrapper detected
if (isotopeWrappers.length < 1) {
return console.log('No isotope wrapper found. Make sure to add the ".isotope-wrapper" class to the main isotope wrapper');
};
// Default variable
// buttons
var filterRes = true;
var filterSelector = "*";
// search
var filterSearch = true;
var qsRegex;
// range
var filterRange = true;
var filterRangeValue = '*';
// checkbox
var filterCheckbox = true;
var filterCheckboxValue = '*';
// radio
var filterRadio = true;
var filterRadioValue = '*';
// Loop inside each isotope wrapper
isotopeWrappers.forEach(wrapper => {
// Set variable and Error Handling
var isotopeContainer = wrapper.querySelector('.isotope-container');
if (!isotopeContainer) {
return console.log('No isotope container found. Make sure to add the ".isotope-container" class to the container of your selectors');
}
var isotopeSelector = isotopeContainer.querySelectorAll('.isotope-container .isotope-selector');
if (isotopeSelector.length < 1) {
return console.log('No isotope selector found. Make sure to add the ".isotope-selector" class to all your selector');
}
var buttons = wrapper.querySelectorAll(".filterbtn-container .filterbtn");
// Show a message in console if no buttons have been found
if (buttons.length < 1) {
console.log('No filter wrapper or filter buttons found. Make sure your filter wrapper has the class ".filterbtn-wrapper" and all your filter buttons have the class ".filterbtn"');
}
var quickSearch = wrapper.querySelector('#quicksearch');
// Show a message in console if no search input have been found
if (!quickSearch) {
console.log('No QuickSearch found. Make sure your search input has the ID "#quicksearch"');
}
var range = wrapper.querySelector('#range');
// Show a message in console if no range slider have been found
if (!range) {
console.log('No Range found. Make sure your range input has the ID "#range"');
}
var checkboxes = wrapper.querySelectorAll('.checkbox');
// Show a message in console if no checkboxes have been found
if (checkboxes.length < 1) {
console.log('No checkbox found. Make sure your checkbox input has the class ".checkbox"');
}
var radios = wrapper.querySelectorAll('.radio');
// Show a message in console if no radio buttons have been found
if (checkboxes.length < 1) {
console.log('No radio found. Make sure your radio input has the class ".radio"');
}
// Gutter Settings through data-gutter
if (wrapper.dataset.gutter) {
var isotopeGutter = parseInt(wrapper.dataset.gutter);
wrapper.style.setProperty('--gutter', isotopeGutter + 'px');
isotopeSelector.forEach(elm => elm.style.paddingBottom = isotopeGutter + 'px');
} else {
// Default option
var isotopeGutter = 0;
console.log('No data-gutter attribute has been found on your isotope container. Default set to 0.');
};
// Layout Settings through data-filter-layout
if (wrapper.dataset.filterLayout) {
var isotopeLayoutHelper = wrapper.dataset.filterLayout;
} else {
// Default option
var isotopeLayoutHelper = 'fitRows';
console.log('No data-filter-layout attribute has been found on your isotope container. Default set to "fitRows".');
};
// init Isotope
var isotopeOptions = {
itemSelector: '.isotope-selector',
layoutMode: isotopeLayoutHelper,
filter: (itemElem1, itemElem2) => {
const itemElem = itemElem1 || itemElem2;
// tags/buttons
if (buttons.length > 0) {
filterRes = filterSelector != '*' ? itemElem.dataset.filter.includes(filterSelector) : true;
}
// quicksearch
if (quickSearch) {
filterSearch = qsRegex ? itemElem.textContent.match(qsRegex) : true;
}
// range
if (range) {
filterRange = filterRangeValue != '*' ? parseInt(itemElem.dataset.range) <= filterRangeValue : true;
}
// checkboxes
if (checkboxes.length > 0) {
filterCheckbox = filterCheckboxValue != '*' ? filterCheckboxValue.includes(itemElem.dataset.checkbox) : true;
}
// radio
if (radios.length > 0) {
filterRadio = filterRadioValue != '*' ? filterRadioValue.includes(itemElem.dataset.radio) : true;
}
return filterRes && filterSearch && filterRange && filterCheckbox && filterRadio;
}
};
// Set the correct layout
switch (isotopeLayoutHelper) {
case 'fitRows':
isotopeOptions.fitRows = {
gutter: isotopeGutter
};
break;
case 'masonry':
isotopeOptions.masonry = {
gutter: isotopeGutter
};
break;
}
var iso = new Isotope(isotopeContainer, isotopeOptions);
// debounce so filtering doesn't happen every millisecond
const debounce = (fn, threshold) => {
var timeout;
threshold = threshold || 200;
return function debounced() {
clearTimeout(timeout);
var args = arguments;
var _this = this;
function delayed() {
fn.apply(_this, args);
}
timeout = setTimeout(delayed, threshold);
};
};
// Event Listener for buttons
if (buttons.length > 0) {
buttons.forEach(elem => elem.addEventListener("click", (event) => {
event.preventDefault();
// get the data-filter attribute from the filter button
var filterValue = event.target.getAttribute("data-filter");
filterSelector = filterValue;
// filter results
iso.arrange();
}));
};
// Event Listener for search input
if (quickSearch) {
quickSearch.addEventListener('keyup', debounce((event) => {
qsRegex = new RegExp(quickSearch.value, 'gi');
//filter the store list
iso.arrange();
}, 200));
}
// Event Listener for range slider
if (range) {
const rangeFilterFN = (e) => {
filterRangeValue = parseInt(e.target.value);
//filter the store list
iso.arrange();
}
range.addEventListener('input', debounce((e) => {
rangeFilterFN(e);
}, 200));
range.addEventListener('keyup', debounce((e) => {
rangeFilterFN(e);
}, 200));
}
// Event Listener for checkboxes
if (checkboxes) {
checkboxes.forEach(checkbox => {
checkbox.addEventListener('click', debounce(() => {
arrCheckbox = [];
checkboxes.forEach(cb => {
if (!cb.checked) {
return;
}
arrCheckbox.push(cb.value);
})
filterCheckboxValue = arrCheckbox;
//filter the store list
iso.arrange();
}, 200));
})
}
// Event Listener for radio buttons
if (radios) {
radios.forEach(radio => {
radio.addEventListener('change', debounce(() => {
arrRadio = [];
radios.forEach(radio => {
if (!radio.checked) {
return;
}
arrRadio.push(radio.value);
})
filterRadioValue = arrRadio;
//filter the store list
iso.arrange();
}, 200));
})
}
const radioButtonGroup = (buttonGroup) => {
buttonGroup.addEventListener("click", (event) => {
buttons.forEach(btn => btn.classList.remove("filterbtn--active"));
event.target.classList.add("filterbtn--active");
});
};
// change is-checked class on buttons
for (var i = 0, len = buttons.length; i < len; i++) {
var buttonGroup = buttons[i];
radioButtonGroup(buttonGroup);
};
});
});
Conclusion
If everything worked as expected, you should see this result on frontend:
Now you have a super lightweight fully-custom bricks-compatible facet function that you can add to your website. Cool isn’t it? ☺