Lifetime prices rise on 19 October →
Phox Elements

Phox Facets

AJAX filtering for any Breakdance, Oxygen or Bricks loop. Drop facet elements anywhere on the page, point them at a loop, and shoppers narrow results without a page reload — with live counts, shareable URLs and a mobile filter panel.

15 facet elements Products, posts & any CPT Taxonomy, meta & attributes Shareable URLs

Overview

Facets are filter controls that live outside the loop they filter. You place them anywhere on the page — a sidebar, a header bar, inside a Filter Panel — and they rewrite the loop's query over AJAX as the visitor clicks.

There are three kinds of element involved, and it helps to keep them straight:

KindExamplesRole
The loopPosts List, Post Loop, Products List, Shop Page, Phox Product GridDisplays the results. Becomes a target automatically.
Input facetsCheckbox, Dropdown, Radio, Search, Range Slider, Swatches, Toggle, Date Range, A–Z, MapCollect the visitor's choices and filter the loop.
Display facetsResults Count, Pagination, Active Filters, ResetReport on, or clear, the current filter state.

Filter anything

Taxonomies, custom fields, WooCommerce attributes, price, sale status, dates, coordinates and free-text search.

No page reload

One AJAX call re-renders the loop server-side and returns fresh HTML, counts, ranges and pagination.

Live counts

Every option shows how many results it would return, recalculated against the other active filters.

Shareable URLs

Every filter state is written to the address bar, so links can be shared, bookmarked and indexed.

Requirements

RequirementNotes
Phox ElementsFacets are built in — there is no separate plugin to install. The Oxygen build ships the same feature set.
Breakdance or Oxygen 6+Facets hook into the builder's query controls, so a builder-rendered loop is required.
WooCommerceOnly for product filtering — price, sale, featured, attributes and the Shop Page element. Post filtering needs no WooCommerce.

Each facet element can be switched off individually under Settings → Phox Elements if you don't want it cluttering the builder's element list.

Quick start

The fastest useful setup: a product grid with a category filter and a result count.

  1. Start with a loop. Add a Products List, Shop Page, Posts List, Post Loop or Phox Product Grid to the page and configure its query as you normally would. Facets take over the filtering, not the query itself.
  2. Add a facet. Drop a Facet — Checkbox next to it. Set Data Source to Taxonomy and pick product_cat.
  3. Name it. Set Facet Name to something short and URL-friendly, e.g. category. This becomes part of the address bar, so keep it readable.
  4. Add a Results Count above the grid so visitors can see the effect.
  5. View the page. Tick a category — the grid refreshes in place, the count updates, and the URL gains ?_pf_category=shirts.
Tip

You don't need to set a Target ID for a single loop. Leave it empty and the facet filters whatever loop is on the page. Target IDs only matter when there's more than one — see Loops & targets.

How filtering works

Worth understanding, because almost every support question comes back to it. Facets run down two different paths that must produce identical results.

1. Full page load

Filter values are read from the _pf_* URL parameters. The loop runs as a normal query and the filters are applied to it before it executes. This is what happens when someone opens a shared link, hits back, or lands from Google.

2. AJAX refresh

Clicking a facet posts the current values to the server, which re-renders just that loop element and returns its HTML along with recalculated counts, ranges and pagination. The browser swaps the loop's markup and updates the URL.

Why this matters

An AJAX request has no idea which page it came from — on the server it is not "the shoe category page", it's just an admin request. The plugin captures the archive context when the page first renders and sends it back with every request so scope is preserved. If filtering ever shows results from outside the current category, that context is what has gone missing — see Archives & category pages.

Counts

Option counts use "OR-back" logic: each facet's counts are calculated against everything matching the other active filters, but not its own. That's why ticking one colour doesn't make every other colour read zero — you can still see what else is available, while counts still narrow as you filter on other facets.

Loops & targets

These loop elements become filter targets automatically:

ElementBuilder
Posts ListBreakdance / Oxygen core
Post LoopBreakdance / Oxygen core
Products ListBreakdance / Oxygen core (WooCommerce)
Shop PageBreakdance / Oxygen core (WooCommerce)
Phox Product GridPhox Elements
Phox Post Loop, Visual Grid LoopPhox Elements

Other elements can be added by a developer — see Custom loop elements.

Target ID

Every supported loop gains a Target ID field (under the element's own Phox Facets section), and every facet has a matching Target ID under Advanced.

  • Leave both empty when there's one loop on the page. Everything wires itself up.
  • Set a matching ID on the loop and on each facet when a page has two or more loops — for example a "New in" grid and a "Sale" grid, each with its own filters.
Keep target IDs unique

Two loops sharing a Target ID — including across a template and a Global Block — resolve to a single target, and the last one found wins. If a facet is refreshing the wrong grid, duplicate IDs are the first thing to check.

The facet elements

Input facets Filters

ElementWhat it doesFilters by
CheckboxThe workhorse. A list of options the visitor can tick, with counts, hierarchy, search-within and AND/OR logic.Taxonomy, meta, attributes
RadioSame options list, one choice only, with an "All" entry to clear.Taxonomy, meta, attributes
DropdownA compact select. Best where space is tight or the list is long.Taxonomy, meta, attributes
Colour SwatchColour chips or image tiles from term meta. Single or multi-select.Taxonomy
ToggleA single on/off switch — "On sale", "In stock", "Featured".Meta, taxonomy term, on-sale, featured
Range SliderA numeric range with optional min/max inputs, prefix and suffix.Price, meta, numeric taxonomy
SearchFree-text search over the loop, debounced as the visitor types.Keyword
Date RangeStart/end date pickers, including an overlapping-range mode for bookings.Date meta
A–Z ListingAn alphabet strip filtering by first letter of the title.Post title
MapAn interactive map: filter the loop by the visible area, or show filtered results as pins.Lat/lng meta, ACF map field
SortChanges the order of results rather than narrowing them.Any orderby

Display facets Output

ElementWhat it does
Results Count"Showing 42 results", with separate templates for plural, single and none.
PaginationNumbered pages, Previous/Next only, or Load More — all AJAX.
Active FiltersChips for every active filter, each removable, plus a Clear All.
ResetA single button that clears everything. Can hide itself when nothing is filtered.

Settings every facet shares

SettingPurpose
Facet NameThe identifier used in the URL (_pf_{name}) and in logic. Keep it short and lowercase. Must be unique on the page.
TitleA heading shown above the facet.
Target IDWhich loop to filter. Empty targets every loop.
Show Reset Button / Reset TextA per-facet clear link, separate from the page-level Reset element.
Initially CollapsedRender the facet closed, so long sidebars stay scannable.

Data sources

Option-based facets (Checkbox, Radio, Dropdown, Colour Swatch) all ask the same first question: where do the options come from?

SourceUse forThen set
TaxonomyCategories, tags, brands, and WooCommerce attributes (pa_colour, pa_size).Taxonomy — the dropdown lists every public taxonomy plus all pa_* attributes.
Custom Field (Meta)Anything stored in post meta — an ACF select, a bespoke field.Meta Key, plus your own Options list of label/value pairs.
Manual optionsA fixed list that doesn't come from the database.The Options repeater — Label and Value per row.
Naming shortcut

If you name a facet after a taxonomy, it's detected automatically with no configuration. Prefixes work too: tax_brand filters the brand taxonomy, meta_colour filters the colour meta key, and pa_size resolves to the WooCommerce Size attribute.

Terms, counts & order

Taxonomy-sourced facets share a set of controls for shaping the list:

SettingEffect
Show CountShow the number of matching results beside each option, updated live as other facets change.
Hide EmptyDrop terms with no results in the current context.
Hide Facet When No OptionsHide the whole facet if nothing is left to choose — keeps sidebars tidy on sparse categories.
Exclude TermsA comma-separated list of slugs or IDs to leave out.
Parent TermShow only children of one term — e.g. only sub-categories of "Footwear".
Show Hierarchy / Hide ChildrenIndent child terms under their parents, or flatten the list to top level only.
LimitCap how many options render.
SearchableAdds a search box inside the facet (Checkbox and Radio) for long lists.
Order By / OrderName, Count, ID, Slug or Menu Order, ascending or descending.
Custom term order

Order By → Menu Order uses a per-term sort value, so you can force a deliberate sequence — sizes as S, M, L, XL rather than alphabetically as L, M, S, XL.

AND / OR logic

Multi-select facets (Checkbox, Colour Swatch) have a Logic setting that decides how several ticks combine:

LogicMeaningGood for
OR (Any selected)Show results matching any ticked option. Ticking more options widens the results.Colours, brands, categories — "show me red or blue"
AND (All selected)Show only results matching every ticked option. Ticking more narrows.Features and tags — "waterproof and insulated"

OR is the default and is what shoppers expect from a colour or size filter. Reach for AND when the terms describe capabilities a single item can hold all of at once.

Facets are always combined with each other using AND: picking a colour and a size narrows to items matching both.

Sorting

The Sort facet renders a dropdown of orderings you define. Each option carries a Label, a Value, an Order By, an Order and optionally a Meta Key for sorting on a custom field.

It also has a Default Option, which decides what's selected before the visitor chooses anything.

Put the Sort facet above its loop

A Sort facet placed below the loop it controls can't apply its default to the first page load — the loop has already rendered by the time the sort element is reached. It works correctly from the first click onwards, but for a default ordering to apply on landing, the Sort element needs to come first in the page order.

WooCommerce default ordering

On shop and category pages WooCommerce applies its own catalogue ordering. When no sort facet is active that default is preserved, including "Menu order" — so an un-filtered archive keeps the sequence you arranged in Products → Sorting. As soon as a sort facet has a value, it takes over.

Pagination & results count

Pagination

The Pagination facet replaces the loop element's own pagination with an AJAX version. Three types:

TypeBehaviour
NumberedClassic page numbers, with optional First/Last links, a configurable number of Pages Each Side, and a Show All Pages option.
Previous / Next OnlyJust the two arrows, with your own Previous Text and Next Text.
Load MoreA button appending the next page to the results. Optionally shows a remaining count — "24 more products".

Scroll to Top returns the visitor to the top of the results after a page change, which matters most on long grids.

Only one pagination

Adding a Facet Pagination element suppresses the loop element's built-in pagination automatically, so you never get two sets of page links.

Results count

The Results Count facet has three templates, each using {count} as the placeholder:

TemplateDefault
PluralShowing {count} results
SingleShowing {count} result
NoneNo results found

The same live count drives the Filter Panel's footer button, so "Show 42 results" stays in step with the grid.

Active filters & reset

Active Filters renders a chip for each applied filter, each with its own remove button, plus an optional Clear All. It can Hide When Empty so it takes no space on an unfiltered page.

Reset is a single button that clears every facet at once. Hide When No Filters keeps it out of the way until it's useful, and Show Icon adds a small clear icon.

Tip

Active Filters is the single best addition to a filtered page. Visitors who can see what they've applied filter more confidently, and it removes most "why am I seeing no results" confusion.

Filter Panel New

The Filter Panel is a container that behaves like an ordinary div on desktop and collapses into an off-canvas drawer below a breakpoint you choose — so the same set of facets serves both layouts.

Before it existed, a responsive filter layout meant building your facets twice: once inline for desktop, once inside an off-canvas panel for mobile, with two sets of Facet Names to keep in step. The Filter Panel removes that duplication entirely.

Why one set of facets is enough

Facets talk to their loop by Target ID over AJAX — they don't care where they sit in the page. Moving them into a fixed-position drawer changes nothing about how they filter. That's what makes a single instance workable in both layouts.

Building one

  1. Add the Filter Panel where the filters should sit on desktop — typically a sidebar column or a bar above the grid.
  2. Put your facets inside it. It's a normal container, so drag in Checkbox, Range Slider, Sort, Reset and anything else, in any arrangement.
  3. Choose the breakpoint at which it becomes a panel.
  4. Add a trigger button anywhere on the page and give it the Open Class.
  5. Style each state separately using the On-screen Div and Off-screen Panel sections.

Breakpoint & modes

Becomes a Panel At is the central setting. At or below the width you pick, the container turns into an off-canvas drawer; above it, it's a plain container in the page.

OptionPanel appears at
Tablet Landscape1119px and below
Tablet Portrait1023px and below
Phone Landscape767px and below
Phone Portrait479px and below
CustomA width you type, via Custom Breakpoint (px)
Always a panelEvery width — behaves like a conventional off-canvas panel
NeverNo panel at all — a plain container

Panel Position slides it in from the Left, Right, Top or Bottom. Left and Right take a Width; Top and Bottom take a Height.

Open In Builder

The panel is closed by default, which makes it awkward to style. Switch the builder to a breakpoint below your chosen one and turn Open In Builder on to hold it open while you work.

Turn it off before publishing

This setting also starts the panel open on the live site. It's a design aid, not a front-end option.

What visitors get

In panel mode the drawer closes on the close button, on the backdrop, on Esc, and after tapping the footer button. The page behind it is scroll-locked while it's open — and if the window is resized back above the breakpoint while open, the panel closes itself and releases the lock rather than leaving the page stuck.

The trigger button

The panel has no button of its own — you build one from any element you like (a Button, a Fancy Button, a Div with an icon) and connect it with a CSS class.

  1. Set the Open Class on the Filter Panel, e.g. openfilters (the default).
  2. Add that class to your button under its own Advanced → CSS Classes.
  3. Optionally set a Close Class and add it to any element that should close the panel.

Any number of elements can carry the Open Class, and because the binding is delegated, buttons added to the page later — including ones inside AJAX-refreshed markup — still work.

Hide Trigger Above Breakpoint

On by default. The trigger is usually only wanted where the panel exists, and this hides any element carrying the Open Class above the breakpoint, so you don't have to set its responsive visibility by hand.

It's a global rule

The hiding rule targets the class itself, not just elements inside the panel. If you reuse the same class for something that should stay visible on desktop, give the trigger its own class instead.

Turn on Show Footer Bar and the panel gains a bar pinned to its bottom edge with a single button — the pattern every large retailer uses on mobile.

Footer Button Text accepts {count}, which is replaced with the live results total:

Show {count} results     →  "Show 42 results"

The count tracks the filtered loop as the visitor changes filters inside the panel, so they can see the effect before committing. Tapping the button closes the panel.

Where the count comes from

It reads whichever Results Count element is on the page, and also listens for the facets' own refresh event. Either source keeps it accurate; you don't need a visible Results Count element for the button to work, but having one is the most reliable setup.

The footer's background, button colours and corner radius are all in Design → Footer Bar.

Styling both states

Because one element renders two quite different things, the design controls are split to match:

SectionAppliesControls
On-screen DivAbove the breakpoint, in the pageBackground Colour, Width, Layout
Off-screen PanelAt or below the breakpoint, as a drawerBackground Colour, Width, Height (Top / Bottom), Layout
Close ButtonPanel onlyHide, Background Color, Icon Color, Size
Footer BarPanel onlyBackground, Button Background, Button Text Color, Button Radius
Blur SettingsPanel onlyBlur Amount behind the open panel

Two Layout controls

Each state has its own Layout popout, which is the point: a horizontal row of filters across the top of the page on desktop, and a vertical stack inside the drawer on mobile, from a single set of facets. Set each independently — direction, alignment and gap.

Sensible default

If you leave the panel's Layout alone, filters stack vertically inside the drawer regardless of the on-screen arrangement. A horizontal row inherited into a 320px drawer would squash the facets to nothing, so the panel stacks by default and only follows its own Layout when you set one.

Widths

The two Width controls are separate for the same reason. The on-screen width sizes the container in the page — useful when it sits in a centred column and you want it to fill the available space. The panel width sizes the drawer. Both are responsive, and the on-screen width is capped at 100% of its column so a large fixed value can't overflow the page.

Range slider

A two-handle slider for numeric filtering. Data Source chooses what it reads:

  • Price — WooCommerce product price, sale-aware.
  • Custom Field (Meta) — any numeric meta key.
  • Taxonomy — numeric term names, for stores that store sizes or capacities as terms.

Min Value, Max Value and Step set the bounds; leave min and max empty to have them calculated from the current results and re-calculated as other filters narrow the set. Prefix and Suffix add units — £ or cm. Show Min/Max Inputs adds number boxes beside the slider for precise entry.

Numeric terms

Taxonomy ranges parse term names that aren't plain integers, including European decimal formats — 113,5cm and 1.200,50 both read correctly.

The slider fires on release rather than on every pixel of movement, so dragging doesn't flood the server with requests.

Date range

Filters on a date stored in post meta. Fields chooses whether to show both a start and an end picker or just one.

SettingPurpose
Meta KeyThe field holding the date.
Compare ModeHow the visitor's range is matched. Overlapping Range (Bookings) matches anything whose own start/end straddles the chosen dates — the right mode for availability.
End Date Meta KeyThe second field, when each item has its own start and end.
Date Storage FormatHow the date is stored — commonly Y-m-d. Must match your data or nothing will match.
Display FormatHow the date is shown to the visitor, e.g. M j, Y.
Min Date / Max DateLimits on what can be picked.
Start / End PlaceholderPrompt text in the empty fields.
Storage format matters most

Date filtering compares strings, so Date Storage Format has to describe exactly how the value sits in the database. An ACF date field set to d/m/Y will not filter correctly against a facet configured for Y-m-d.

Colour swatches

Renders taxonomy terms as colour chips or image tiles instead of checkboxes. The colour or image comes from term meta, which the plugin adds to the term edit screen for you — open any term under Products → Attributes or a taxonomy and you'll find the swatch fields there.

SettingPurpose
Selection ModeMulti-select (checkbox behaviour) or Single-select (radio behaviour).
All Option Label / Color / ImageIn single-select mode, an explicit "All" chip to clear the filter.
Show LabelShow the term name under each chip as well as the colour.

Everything from Terms, counts & order applies here too — counts, hide empty, exclude, parent term, limit and ordering.

A–Z listing

An alphabet strip that filters by the first letter of the post title — for directories, glossaries, brand indexes and staff listings.

  • Post Type — which post type the letter pool is built from.
  • Show Counts — the number of entries behind each letter.
  • Show # (Numbers) — a # entry gathering titles that start with a digit.
  • All Label — the text of the "show everything" entry.

Letters with no entries are rendered inactive rather than hidden, so the strip keeps a stable width as the visitor filters.

Map

The Map facet works in both directions: it can filter the loop to whatever is inside the visible map area, and it shows the currently filtered results as pins.

Coordinates

Coordinate SourceSet
Separate Lat/Lng FieldsLatitude Meta Key and Longitude Meta Key.
ACF Google Map FieldACF Google Map Field Key — the single field holding both values.

Provider & appearance

Choose Leaflet / OpenStreetMap (no key required) or Google Maps (set the Phox Google Maps API Key under Breakdance → Settings → API Keys). Map Type offers Roadmap, Satellite, Hybrid and Terrain, and Default Zoom, Default Latitude and Default Longitude set the opening view.

Pins & clusters

Pins can be a coloured marker (with Pin Color, width and height) or an image, including a per-post pin image pulled from a meta key so each result can carry its own thumbnail — with border width, colour, radius, background and padding controls for framing it.

Marker Clustering groups nearby pins, with cluster colour, text colour and size, and an optional Vary Color by Count so busier clusters use a High-Count Color above a threshold you set.

Popups

Popup Content chooses between Title Only, Title + Excerpt, Title + Thumbnail + Excerpt, or no popup. Excerpt Format decides whether the excerpt renders formatted, exactly as it appears on the post, or as plain trimmed text with an Excerpt Length word cap.

Tip

Auto-fit Bounds on Filter re-frames the map around whatever survives the current filters, so the visitor never has to hunt for the remaining pins. Filter Debounce controls how quickly panning the map triggers a refresh.

URLs & shareable filters

Every filter change is written to the address bar using the facet's own name with a _pf_ prefix:

/shop/?_pf_category=shirts,coats&_pf_price=20-80&_pf_sort=price_asc
PatternMeaning
_pf_{name}=slugA single value.
_pf_{name}=a,b,cSeveral values in a multi-select facet.
_pf_{name}=20-80A range facet's lower and upper bound.
/page/2/Pagination, kept as a normal path so links remain crawlable.

Because state lives entirely in the URL, filtered pages can be shared, bookmarked, emailed to a customer, linked from an ad, or used as a landing page. Back and forward work as visitors expect — the browser's history is updated on every change and the page restores that exact state.

The prefix is stable

_pf_ won't change between releases, so links you've published or hard-coded stay valid.

Archives & category pages

Facets work on ordinary pages and on archive templates — a shop page, a product category, a taxonomy archive, a post type archive. On an archive, the archive's own scope is preserved: filtering inside Footwear narrows within Footwear rather than escaping to the whole catalogue.

This holds even when a facet filters the very taxonomy the archive is built on. A category facet on a category archive filters within that branch.

If results escape the category

This is the one failure that looks alarming and has a single cause: the archive context isn't reaching the server on the AJAX request. It usually means the loop element builds its query in a way the plugin can't see. Check Troubleshooting, and note that the standard loop elements and Phox Product Grid all handle it.

Per-category template overrides

If a specific category uses its own Breakdance template, make sure the loop in that template is one of the supported elements and that Target IDs don't collide with the generic template's. A page that resolves several templates can otherwise end up targeting a loop that isn't the one on screen.

Caching

Filtered pages must not be cached, or visitors get someone else's results. The plugin handles this for you: when any _pf_* parameter is present it sets DONOTCACHEPAGE, sends no-cache headers, and disables WordPress's canonical redirect so filter parameters aren't stripped.

That covers every cache plugin that respects the standard constant, and most hosting page caches. If a host's edge cache still serves stale filtered pages, add an exclusion rule for URLs containing _pf_.

Script concatenation

Some optimisation plugins concatenate JavaScript into a single bundle. If facets stop responding after enabling one, exclude the Phox Elements scripts from concatenation — a failed bundle takes the facets JS down with it.

Oxygen

The Phox Oxygen plugin carries the same facet elements and the same filtering engine, so everything in this document applies there too. The differences are cosmetic: element names appear in Oxygen's element panel, and the generated CSS uses Oxygen's class naming.

Configurations aren't portable between the two builders — a page built in Breakdance has to be rebuilt in Oxygen — but the facet settings themselves are identical, so there's nothing new to learn.


Architecture

PHP lives in includes/phox-facets/ under the PhoxFacets namespace; the front-end runtime is assets/phox-facets.js; the UI elements are elements/Facet_*.

ClassResponsibility
FacetRegistryHolds current facet values. Parses _pf_* parameters, sanitises them, and mirrors them into $_GET so SSR files can read either source.
QueryModifierApplies values to queries. Hooks breakdance_query_control_query and breakdance_woocommerce_get_products_query at priority 5, plus pre_get_posts for the main archive query.
AjaxHandlerHandles phox_facets_filter: re-renders one element's ssr.php in an output buffer and returns HTML, counts, ranges and pagination.
BreakdanceIntegrationTarget detection, the Target ID control, archive-context capture, and the footer config script.
FacetRendererRe-renders the display facets (Pagination, Active Filters, Results Count, Reset, A–Z) on each response.
CountPool / TermOrder / SwatchTermMetaCount calculation, custom term ordering, and the swatch colour/image term meta fields.

Query types

Each facet resolves to one query type, either from its own configuration or by detection from its name:

tax_query · meta_query · search · orderby · price_range
on_sale · featured · tax_range · title_alpha · date_range · geo_bounds
Ordering invariant

Facet filters run at priority 5; archive scope is re-applied at priority 100. This ordering is deliberate — a facet that owns a taxonomy strips existing clauses for it so URL-landing filters don't double-constrain, and the archive term must be added back after that. Never add archive scope early in an element's query build; it will vanish the moment someone adds a facet on the same taxonomy.

Front-end config

The plugin prints a configuration object in the footer of any page carrying facets:

window.PhoxFacetsConfig = {
    ajaxUrl,            // endpoint
    targets,            // [{ postId, elementId, ssrPath, targetId, elementSlug }]
    initialPagination,  // { found_posts, max_pages, current_page }
    paramPrefix,        // '_pf_'
    archiveContext      // { type:'term'|'shop'|'post_type_archive', ... }
};

It's the first thing to inspect when debugging. An empty targets array means no supported loop was found; a missing or wrong archiveContext explains scope escaping on archive pages.

Each AJAX request posts postId, elementId, ssrFilePath, paged, facets, facetConfigs and archiveContext, and the response carries html, maxPages, foundPosts, currentPage, facets, facetCounts, facetRanges and mapMarkers.

Hooks & filters

// Add a loop element to the supported-target list.
add_filter( 'phox_facets_supported_elements', function ( $slugs ) {
    $slugs[] = 'MyPlugin\\MyLoop';
    return $slugs;
} );

// Tell the AJAX handler which ssr.php to render for it.
add_filter( 'phox_facets_element_ssr_path', function ( $path, $slug ) {
    return $slug === 'MyPlugin\\MyLoop'
        ? MY_PLUGIN_DIR . 'elements/MyLoop/ssr.php'
        : $path;
}, 10, 2 );

// Allow that path through the SSR allowlist.
add_filter( 'phox_facets_allowed_ssr_paths', function ( $paths ) {
    $paths[] = MY_PLUGIN_DIR . 'elements/MyLoop/ssr.php';
    return $paths;
} );

// Supply a config for a facet name the detector cannot resolve.
add_filter( 'phox_facets_detect_config', function ( $config, $name, $value ) {
    if ( $name === 'region' ) {
        return [ 'query_type' => 'tax_query', 'taxonomy' => 'area' ];
    }
    return $config;
}, 10, 3 );

// Stop a secondary query overwriting the captured pagination data.
add_filter( 'phox_facets_should_capture_query', function ( $should, $query ) {
    return $query->get( 'my_widget_query' ) ? false : $should;
}, 10, 2 );
FilterPurpose
phox_facets_supported_elementsWhich element slugs become filter targets.
phox_facets_element_ssr_pathMap an element slug to the SSR file rendered on AJAX.
phox_facets_allowed_ssr_pathsAllowlist of SSR files the handler may include.
phox_facets_detect_configResolve a facet name to a query configuration.
phox_facets_should_capture_queryVeto a query from providing pagination/count data.
phox_facets_count_pool_args · _ids · _maxTune the post pool that counts are calculated from.
phox_facets_term_order_meta_keyThe meta key holding custom term order.
phox_facets_pvbur_compatCompatibility shim for role-based pricing/visibility plugins.
SSR allowlist

The AJAX handler only ever includes files that appear in its allowlist. Adding a path through phox_facets_allowed_ssr_paths makes that file includable from a public request, so point it only at builder SSR templates you control.

Custom loop elements

To make your own loop element filterable:

  1. Register it as a target with phox_facets_supported_elements. It then gets the Target ID control and the target classes automatically.
  2. Point the handler at its SSR file with phox_facets_element_ssr_path, and allow that path with phox_facets_allowed_ssr_paths.
  3. Build the query through the builder's query control so the facet filters actually run. If you build a raw WP_Query from control args instead, those filters never fire and you must apply $_POST['archiveContext'] yourself.
  4. Leave the query in scope. The handler collects pagination from a $loop WP_Query variable left behind by the SSR file.

Writing a facet element

Follow Facet_Checkbox — it's the canonical implementation. The contract is: enqueue phox-facets, render a wrapper carrying data-phox-facet, data-phox-facet-name and a data-phox-facet-config JSON blob, read the current value from FacetRegistry, and register the facet with QueryModifier so full page loads filter correctly.


Troubleshooting

The facet does nothing at all

Check window.PhoxFacetsConfig in the browser console. If targets is empty, no supported loop was detected — confirm the loop is one of the supported elements. If targets exist but the facet is inert, check its Facet Name is set and that its Target ID (if any) matches the loop's exactly.

Filtering shows results from outside the current category

The archive context isn't surviving the AJAX request. Inspect PhoxFacetsConfig.archiveContext — on a category page it should read type: "term" with the correct term_id. If it's empty, the page isn't resolving as an archive. If it's correct but results still escape, the loop element is building its query outside the builder's query control.

Sorting reloads the grid but the order never changes

WooCommerce's catalogue ordering applies its ordering at SQL level and overrides plain query arguments. The plugin bypasses that path when a sort facet is active — if you see this on a custom loop, that element needs the same treatment.

The results count or pagination is stuck on the wrong number

Both read the captured loop data, and a secondary query — a related-products block, a second loop — can overwrite it. Exclude that query with phox_facets_should_capture_query. If the count element sits above the loop it will have no data on first render; the JS corrects it immediately after load.

Counts look wrong on a numeric taxonomy

Term names that aren't plain integers are parsed for their numeric value, including European decimal formats. If counts still look wrong, check the term names for stray characters before suspecting the query.

Filtering lands on a 404, or the parameters are stripped

WordPress's canonical redirect is disabled when facet parameters are present, so this is nearly always a host or edge cache serving a redirect it cached earlier. Exclude URLs containing _pf_ from the cache.

The Filter Panel doesn't open

Confirm the trigger element actually carries the Open Class — check it in the page source, not just in the builder. If Hide Trigger Above Breakpoint is on, remember the button is deliberately hidden on desktop; test at a width below your breakpoint.

The panel opens but sits behind something, or in the wrong place

The panel is fixed-positioned, and a CSS transform, filter or backdrop-filter on any ancestor section creates a new containing block that traps it. Check the parent section for animation or blur effects.

Facets stopped working after enabling an optimisation plugin

Exclude the Phox Elements scripts from JavaScript concatenation and minification. A bundle that fails to build takes the facets runtime with it.

FAQ

Do facets work with posts, or only products?

Any post type. WooCommerce is only needed for product-specific filters — price, sale status, featured and product attributes.

Can one page have two independently filtered loops?

Yes. Give each loop a unique Target ID and set the matching ID on each facet. Facets with an empty Target ID filter every loop on the page.

Do I need to build my filters twice for mobile?

No — that's exactly what the Filter Panel removes. One set of facets renders inline on desktop and as a drawer on mobile.

Can visitors share a filtered view?

Yes. The full filter state lives in the URL, so any filtered page can be copied, shared, bookmarked or used as a landing page.

Will filtered pages get cached and served to the wrong visitor?

No. Pages carrying facet parameters are marked uncacheable and sent with no-cache headers. See Caching.

Why do other options still show a count when I've already filtered?

By design. Counts within a facet are calculated against the other active filters but not that facet's own selection, so visitors can see what else is available rather than facing a column of zeros.

Can I filter on an ACF field?

Yes — set the facet's Data Source to Custom Field (Meta) and enter the field name as the Meta Key. For ACF date fields, match the Date Storage Format to how ACF saves the value.

Can I control the order of sizes rather than sorting them alphabetically?

Yes — set a sort value on each term and choose Order By → Menu Order on the facet.

Does it work on category and shop archive pages?

Yes, and the archive's own scope is preserved — filtering inside a category narrows within it rather than escaping to the whole catalogue.

Is the Oxygen version the same?

Yes. Same elements, same engine, same settings. Only the element panel and generated class names differ.