FacetWP is the default answer for filterable content listings in WordPress, and on the right site it's fine. On sites where the filter UI matters to the visitor experience — content libraries, product catalogs, event listings, knowledge bases — a custom faceted browse built from scratch is often a better fit. Lower payload, exactly your design language, no vendor lock-in, and the architecture pattern is well-understood enough that "build" beats "buy" for any team that's done it once. Here's the pattern, what it costs to build, and when to skip and just use the plugin.
Content-heavy WordPress sites accumulate filterable lists: blog posts that need filtering by category and tag, products by attribute, case studies by industry, events by date and venue, resources by type. The default WordPress experience for filtering these is bad: a chronological list with no filter UI at all, or a generic WP_Query with URL parameters and no client-side feedback.
The market default answer is FacetWP. It’s a competent plugin: it generates a filter sidebar, handles the query, updates the URL, supports AJAX refresh. For sites that need filters and don’t want to think hard about it, FacetWP is genuinely fine. Install it, configure it, ship it.
The reason to consider building custom is when the filter UI is visible enough on the site that vendor defaults stop being acceptable. The chips look generic. The mobile pattern is FacetWP’s, not yours. The performance carries the plugin’s overhead. The integration with your existing design system has friction. And critically, every site using FacetWP looks like every other site using FacetWP — there’s a recognizable visual signature that lands as “this is a stock implementation.”
For sites where the filter UI is a primary product surface, custom is the right call. The pattern is well-understood, but the architecture you choose depends entirely on the size of your dataset.
The Client-Side Architecture (For datasets under ~300 items)
If you are filtering a portfolio of 50 case studies, a directory of 100 staff members, or a library of 200 articles, the custom build is meaningfully smaller than people expect. The architecture looks like this:
- Server-rendered baseline. A custom
WP_Queryrenders all the filterable items as cards in a list or grid. Every card is in the page on initial load. Each card has data attributes for every filterable property:data-category="news",data-industry="healthcare", etc. - Server-rendered sidebar. The filter sidebar lists the available filters with counts computed at render time. Each filter is a real
<a>link to the filtered URL so the baseline experience works without JavaScript. - Vanilla JS interception. A small JavaScript module intercepts filter clicks. Instead of navigating, it filters the visible cards in place by toggling the
[hidden]attribute on cards that don’t match the active filter set. It updates the URL viapushStateso the filtered state is shareable.
Total payload: a few hundred lines of PHP for the rendering, ~3KB of vanilla JS, ~2KB of CSS for the filter UI. No third-party dependencies. No subscription. The site stays on brand because every UI element was built specifically for it.
The Scale Trap (For datasets over 1,000 items)
The architecture described above contains a hidden trap: rendering 5,000 products or 10,000 archival posts into the DOM on initial load will destroy your page performance and bloat the memory footprint until the browser crashes.
If your dataset is massive, you cannot use client-side DOM filtering. You must build a server-side AJAX or REST API architecture. JavaScript intercepts the click, sends the parameters to a custom WordPress REST API endpoint, and the server returns either a JSON payload of results (which JS templates into the grid) or pre-rendered HTML fragments. This is substantially more complex to build from scratch because you must also handle pagination, skeleton loaders, and complex taxonomy intersections.
The patterns that matter.
Whether you build for client-side or server-side, a few specific design decisions make custom-built filtering work better than the default plugin experience:
- Single-select within each dimension; cross-dimension combination allowed. One category + one tag combine; multiple categories at once typically don’t. This is the right default for content sites.
- Gray-out incompatible filters. When one filter is active, chips in the other dimension that would produce zero results get a disabled visual treatment so users can’t construct an empty query.
- Filter count badges. Each chip shows the count of items it would surface. The count is visible breadth: a signal of depth that helps the visitor decide whether to filter.
- Empty state handling. When filters genuinely yield zero, a friendly message + clear-filters action beats a blank list.
- ARIA discipline. Filter chips get
aria-pressed, the result count gets anaria-liveregion, the search input gets a proper label. The custom build can do this perfectly; a plugin’s defaults often fall short.
The mobile pattern.
The desktop sidebar collapses on mobile to a button that opens an inline drawer. The pattern is conventional: a “Filter (N)” button shows the active-filter count; clicking it expands the same sidebar content as a full-width section.
On mobile-heavy sites, this is where the custom build pays back hardest. FacetWP’s mobile pattern works but isn’t customizable to fit a specific brand’s mobile design language. The custom build is exactly what the design specs called for.
What you lose by not using FacetWP.
Honest accounting of what the plugin gives you that the custom build doesn’t:
- Index-table performance for massive datasets. FacetWP creates a custom database table (
wp_facetwp_index) that makes counting and querying facets incredibly fast. Rebuilding that indexing logic for a massive custom WooCommerce store is a serious engineering project. - Editor-friendly configuration. FacetWP has an admin UI where non-developers can set up new filters by clicking. Custom-built means the developer touches code to add a new filter type.
- Advanced filter types out of the box. Sliders for numeric ranges, map bounds, and date pickers. Custom-built means writing each of these input handlers once.
- Cross-CPT support. FacetWP supports filtering across multiple custom post types in one query effortlessly.
For sites where filters change frequently, the dataset is massive, and the marketing team needs to configure them without a developer, FacetWP earns its keep. For sites where the filter set is mostly stable, the dataset is manageable, and the design is the differentiator, custom wins.
The decision framework.
A few questions to decide:
- How big is the dataset? If it’s over 1,000 items, lean FacetWP (or prepare for a much larger custom backend engineering phase). If it’s under 300, lean custom DOM-filtering.
- Is the filter UI a primary product surface? If users interact with it as part of the core experience (like an Airbnb search), lean custom.
- Will non-developers need to add or remove filters frequently? If yes, FacetWP.
- Is the design language strong enough that a stock-looking filter UI would be a regression? If yes, lean custom.
The right answer varies. The point is to actually make the call rather than reaching for the plugin by default because that’s what the WordPress ecosystem usually does.
See Platform architecture built to last for how custom UI patterns fit into a broader site architecture.