custom filter types instead of default facets
The default Layered Navigation filters are fine for simple catalogs, but quickly hit their limits with range sliders, multi-level AND/OR logic, or category specific facets. Truly customizing Layered Navigation in Magento 2 requires understanding FilterList, FilterableAttributeList and the OpenSearch aggregation layer before writing any custom code.
Table of Contents
- 1. Why default filters often fall short
- 2. Architecture: Layer, FilterList and aggregations
- 3. Implementing a custom filter type
- 4. Controlling filter order and visibility
- 5. Integrating swatches correctly into Layered Navigation
- 6. Multi-select filters and AND/OR logic
- 7. Performance: aggregations and facet caching
- 8. Frontend integration with Hyvä and Alpine.js
- 9. Filter approaches compared
- 10. Summary
- 11. FAQ
1. Why default filters often fall short
Layered Navigation is Magento's facet system for category and search result pages. By default it covers select attributes, price ranges and the category hierarchy, all three computed from the search index via aggregation. For a catalog with a few clearly defined attributes, that is usually enough. But once range sliders for technical specs, multi-level combinations of AND and OR logic, or category specific filters are needed, the default Layered Navigation quickly reaches its limits.
A typical real world example: an electronics shop wants to filter processor performance as a numeric range with sliders instead of fixed tiers like a select attribute. A fashion shop wants to combine size and color without one selection invalidating the other when a specific combination is out of stock. Both cases require adjustments to Layered Navigation that go beyond backend attribute configuration.
The effort for these adjustments pays off because Layered Navigation is one of the most heavily used areas of the storefront. Customers who reach a suitable product through filters tend to have a higher purchase probability than customers scrolling through long product lists. Every improvement to filter logic directly affects the conversion rate of category pages.
2. Architecture: Layer, FilterList and aggregations
At the center of Layered Navigation sits the class Magento\Catalog\Model\Layer, which the LayerResolver supplies with the right instance depending on context, category or search result. The actual filter logic is assembled by FilterList, which instantiates the appropriate filter type for every facetable attribute, such as Attribute, Price or Category. Which attributes get considered at all is determined by FilterableAttributeList, which loads the attributes marked in the backend.
At the OpenSearch level, every one of these filters ultimately becomes an aggregation in the search query, usually a terms aggregation for select values or a range aggregation for prices. The count next to every filter option, the hit count per value, comes directly from these aggregations, not from a separate database query. Extending Layered Navigation therefore requires keeping an eye on both the PHP layer, the filter classes, and the query layer, the aggregations, since the two need to stay in sync.
<?php
declare(strict_types=1);
namespace Mironsoft\LayeredNavigation\Model\Layer\Filter;
use Magento\Catalog\Model\Layer\Filter\AbstractFilter;
use Magento\Catalog\Model\Layer\Filter\Item\DataBuilder;
use Magento\Framework\App\RequestInterface;
/**
* Custom range filter for numeric attributes with fixed, business-defined buckets.
*/
final class RangeFilter extends AbstractFilter
{
/**
* Applies the range filter based on the current request and updates the collection.
*
* @param RequestInterface $request Current storefront request
* @return $this
*/
public function apply(RequestInterface $request): static
{
$rangeParam = $request->getParam($this->_requestVar);
if (!$rangeParam) {
return $this;
}
[$from, $to] = array_pad(explode('-', (string) $rangeParam), 2, null);
$this->getLayer()->getProductCollection()->addFieldToFilter(
$this->getAttributeModel()->getAttributeCode(),
['from' => $from, 'to' => $to]
);
$this->getLayer()->getState()->addFilter(
$this->_createItem((string) $rangeParam, $rangeParam)
);
return $this;
}
}
3. Implementing a custom filter type
The RangeFilter above is a starting point for a custom filter type inside Layered Navigation, but still needs a matching Renderer for displaying the options and a dedicated aggregation on the OpenSearch side. For computing the counts, how many products per bucket, Magento's generic range aggregation works fine as long as the bucket boundaries are fixed and don't need to be derived dynamically from the catalog.
Registering a new filter type happens through a custom FilterList extension via di.xml, which instantiates the custom filter instead of the default attribute filter for specific attribute codes. Important for a clean Layered Navigation extension: the new filter must implement the same interfaces as the default filters, so layout templates and widgets can process it without special cases.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\Catalog\Model\Layer\FilterList">
<arguments>
<argument name="filterableAttributes" xsi:type="array">
<item name="cpu_performance" xsi:type="string">
Mironsoft\LayeredNavigation\Model\Layer\Filter\RangeFilter
</item>
</argument>
</arguments>
</type>
</config>
4. Controlling filter order and visibility
The order of filters in Layered Navigation is controlled through the attribute's position in the attribute set, but in many projects this is poorly maintained because it is rarely touched when new attributes are created. For merchandising purposes, a deliberate order pays off: filters with high purchase relevance, such as price and brand, should sit higher than rarely used technical filters.
For making individual filters visible only in certain categories, the default configuration is not enough, since Magento activates facets globally per attribute, not per category. A common solution is a plugin on FilterableAttributeList::getList() that filters attributes based on the current category or its attribute set before they even reach Layered Navigation. That way, filters that make no sense for a particular product group automatically disappear from the display.
5. Integrating swatches correctly into Layered Navigation
Swatches, visual filters for color and pattern, are technically a special form of select attribute and are rendered by Layered Navigation via a dedicated renderer class that loads an image or color value in addition to the text option. A common mistake with custom swatch adjustments: the visual rendering gets changed but the underlying aggregation stays untouched, so the counts next to each swatch no longer match the actual visual display.
For multi swatch attributes, for example color gradients with several color values per product, it is also worth checking the aggregation size (the size parameter of the terms aggregation). Too low a value cuts rarer color options out of Layered Navigation without this being immediately obvious in the frontend, which leads to an incomplete filter list in catalogs with many color variants.
6. Multi-select filters and AND/OR logic
Within a single attribute, Magento's Layered Navigation combines multiple selected values with OR logic by default, a product must have at least one of the selected values, while different attributes are combined with AND logic, a product must satisfy every selected filter at the same time. This behavior is sensible for most catalogs, but falls short in niche cases, for example when size and color together need to describe an actually available stock combination.
{
"query": {
"bool": {
"filter": [
{ "terms": { "color": ["red", "blue"] } },
{ "terms": { "size": ["m", "l"] } }
]
}
},
"aggs": {
"available_combinations": {
"terms": { "field": "color_size_combination", "size": 50 }
}
}
}
For genuine combination logic in Layered Navigation, an additional field computed at indexing time (color_size_combination in the example) helps, mapping every actually stocked color size combination as its own value. This approach avoids complicated nested queries at runtime and shifts the complexity into the indexing process, which typically yields the better performance trade-off.
7. Performance: aggregations and facet caching
Every additional aggregation in Layered Navigation increases the search query's response time, since OpenSearch has to run extra computations over the filtered result set for each aggregation. For catalogs with many facetable attributes, the sum of these aggregations can noticeably contribute to category page load time, particularly under high storefront concurrency.
A proven lever is deliberately capping aggregation sizes (the size parameter) and removing attributes rarely used as filters from Layered Navigation, instead of computing them unused. On top of that, the Full Page Cache can serve category pages without an active filter selection, while filtered views (with query parameters) typically stay uncached, since the combination variety is too large for a meaningful cache hit rate.
8. Frontend integration with Hyvä and Alpine.js
In the Hyvä theme, Layered Navigation is typically passed to the template through a ViewModel class that shapes filter data into a format Alpine.js can consume. For custom filter types like the range slider from section 3, a dedicated Alpine.js component pays off, keeping the slider value local and only submitting the form once the slider is released, instead of triggering a new request on every movement.
<div x-data="rangeFilter({min: 0, max: 1000, current: [100, 600]})">
<input type="range" x-model.number="current[0]" :max="max" @change="submit()">
<input type="range" x-model.number="current[1]" :max="max" @change="submit()">
<span x-text="`${current[0]} - ${current[1]}`"></span>
</div>
<script>
function rangeFilter(config) {
return {
min: config.min,
max: config.max,
current: config.current,
submit() {
// Debounced form submit, avoids one request per pixel of slider movement
window.location.href = `?cpu_performance=${this.current[0]}-${this.current[1]}`;
}
};
}
</script>
9. Filter approaches compared
The following table compares default filters with custom extensions of Layered Navigation.
| Requirement | Default filter | Custom extension | Benefit |
|---|---|---|---|
| Numeric ranges | Fixed select tiers | Range filter with slider | Finer, more intuitive selection |
| Combining color + size | Independent AND logic | Combination field in the index | Only in-stock combinations shown |
| Category specific filters | Active globally per attribute | Plugin on FilterableAttributeList | Relevant filters per category |
| Many facets at once | Every aggregation computed | Deliberate size cap | Shorter response times |
Wherever default filters hit their limits, a targeted, focused extension of Layered Navigation usually pays off more than a full custom rebuild of the facet system.
Mironsoft
Magento search, Layered Navigation and Hyvä frontend development
Filters that actually lead your customers to the product?
We build custom filter types, combination logic and Hyvä ready Alpine.js components for your Layered Navigation, with a focus on performance and merchandising impact.
Custom filters
Range filters, combination logic and category specific facets
Hyvä integration
Alpine.js components for sliders, swatches and multi-select
Performance tuning
Aggregation sizes and cache strategy for fast category pages
10. Summary
Layered Navigation in Magento 2 can be customized far beyond the default configuration once Layer, FilterList and the underlying OpenSearch aggregations are understood as one connected system. Custom filter types like range sliders, category specific visibility, and genuine combination logic for color and size solve concrete merchandising problems that default attribute filters cannot express.
Performance remains the limiting factor throughout: every additional aggregation costs response time, which is why aggregation sizes should be deliberately capped and rarely used filters removed from Layered Navigation. Combined with clean Hyvä integration via Alpine.js, the result is filter logic that is both technically performant and intuitive for customers to use.
Layered Navigation in Magento 2 — Key Takeaways
Understand the architecture
Layer, FilterList and FilterableAttributeList work together, aggregations supply the hit counts.
Custom filter types
Range sliders and combination logic via custom filter classes plus a matching aggregation.
Category specific filters
Via a plugin on FilterableAttributeList, since Magento otherwise controls facets only globally.
Performance first
Cap aggregation sizes, remove unused filters, use the Full Page Cache deliberately.