How to Filter and Sort Posts by Custom Field Value Using JetSmartFilters + Bricks Builder
If you've ever tried to let users filter and sort a post listing by a numeric custom field — say, a product spec, a rating, or a measurement — you've probably hit the wall where JetSmartFilters (JSF) and Bricks Builder don't quite connect out of the box.
This post documents exactly how to wire them together: a radio filter that, when clicked, filters out posts missing that field and sorts the remaining results by that field's numeric value in descending order.
The Goal
A sidebar radio list of custom field names (e.g. "Weight", "Length", "Rating"). When a user selects one:
- Only posts that have a value for that field are shown
- Results are sorted highest to lowest by that field's numeric value
- All other active filters (taxonomy, search, etc.) continue to stack correctly
The Stack
- Bricks Builder — query loop with a custom Query ID
- JetEngine — CPT with custom meta fields (all numeric)
- JetSmartFilters — Radio filter + existing taxonomy filters on the same page
Why This Isn't Straightforward
JSF has a Sort widget and a Radio filter widget, but they operate independently. Clicking a radio does not trigger a sort. You can't natively say "when this radio value is selected, sort by that meta key."
The solution is to use JSF's radio filter to pass a plain query variable, then intercept Bricks' query via the bricks/posts/query_vars filter hook to inject the correct meta_query, orderby, and meta_key — all server-side, no JavaScript needed.
Step 1 — Set Up the Radio Filter in JSF
In JetSmartFilters, create a new Radio filter with the following settings:
- Data Source: Manual Input
- Options List: One entry per custom field. Label = display name, Value = the meta key
Label: Weight Value: weight
Label: Length Value: length
Label: Rating Value: rating
- This filter for: Bricks query loop
- Query ID: your-listing-id (must match the Query ID set on your Bricks loop element)
- Apply on: Value change
-
Query Variable:
_plain_query::sort_by_field
The _plain_query:: prefix is important. It tells JSF to pass the value as a plain query variable rather than attempting its own meta query processing. JSF will internally convert this to _plain_query_sort_by_field in the POST payload.
Step 2 — Note Your Bricks Loop Query ID
In Bricks Builder, select your query loop element and set a Query ID under the JSF settings panel (the field labelled "Query ID" added by JSF when the element is marked as filterable).
Example: my-listing
All JSF filter widgets on the page must have the same Query ID set in their "This filter for" setting.
Step 3 — The PHP Hook
Add this to your theme's functions.php or a site-specific plugin.
add_filter('bricks/posts/query_vars', function($query_vars, $settings, $element_id, $element_name) {
// Target only your specific Bricks loop by its JSF Query ID
// Note: Bricks stores this under 'jsfb_query_id', not 'query_id'
$query_id = $settings['jsfb_query_id'] ?? '';
if ($query_id !== 'my-listing') return $query_vars;
// JSF sends filter data via POST during AJAX requests
// The _plain_query:: prefix becomes _plain_query_ in the POST key
$query_data = $_POST['query'] ?? [];
$sort_field = sanitize_key($query_data['_plain_query_sort_by_field'] ?? '');
if (empty($sort_field)) return $query_vars;
// Whitelist allowed field keys — never trust user input directly
$allowed_fields = ['weight', 'length', 'rating']; // add all your fields
if (!in_array($sort_field, $allowed_fields, true)) return $query_vars;
// Filter: only show posts that have a non-empty value for this field
$query_vars['meta_query'] = [
'relation' => 'AND',
[
'key' => $sort_field,
'compare' => 'EXISTS',
],
[
'key' => $sort_field,
'value' => '',
'compare' => '!=',
],
];
// Sort: numeric descending by the selected field
$query_vars['orderby'] = 'meta_value_num';
$query_vars['order'] = 'DESC';
$query_vars['meta_key'] = $sort_field;
return $query_vars;
}, 99, 4); // Priority 99 is critical — see note below
// Register the query var so WordPress doesn't strip it
add_filter('query_vars', function($vars) {
$vars[] = 'sort_by_field';
return $vars;
});
Critical Lessons Learned
1. The settings key is jsfb_query_id, not query_id
When you check $settings inside the hook, Bricks stores the JSF Query ID under jsfb_query_id. Using query_id will always return empty and your hook will silently skip every request.
To discover the correct key for your setup, temporarily dump the full settings array:
add_filter('bricks/posts/query_vars', function($query_vars, $settings, $element_id, $element_name) {
error_log(print_r($settings, true));
return $query_vars;
}, 10, 4);
2. JSF uses POST, not GET, for AJAX filter requests
When a user clicks a filter, JSF fires an AJAX request. The filter values are in $_POST['query'], not $_GET. Reading $_GET will always be empty on filter interactions.
3. The _plain_query:: prefix transforms the POST key
Setting the Query Variable in JSF to _plain_query::sort_by_field results in the POST key being _plain_query_sort_by_field (the :: becomes _). Read it accordingly:
$query_data = $_POST['query'] ?? [];
$sort_field = sanitize_key($query_data['_plain_query_sort_by_field'] ?? '');
4. Hook priority must be higher than JSF's own hooks
This is the most painful one. If you register the hook at the default priority of 10, JSF will run its own bricks/posts/query_vars hook afterward and overwrite your orderby back to date (the default).
Always use priority 99 or higher when your hook needs to be the final word on query arguments:
}, 99, 4); // Not 10. Not 20. Use 99.
5. The hook signature takes 4 parameters
The Bricks documentation shows 4 arguments: $query_vars, $settings, $element_id, $element_name. The last argument ($element_name) was added in Bricks 1.11.1. Always declare all 4 and pass 4 as the last argument to add_filter:
add_filter('bricks/posts/query_vars', function($query_vars, $settings, $element_id, $element_name) {
// ...
}, 99, 4);
How the Full Flow Works
User clicks radio → "Weight"
↓
JSF fires AJAX: POST query[_plain_query_sort_by_field] = weight
↓
bricks/posts/query_vars hook fires (priority 99)
↓
Hook reads _plain_query_sort_by_field from $_POST['query']
Validates against whitelist
Injects meta_query (EXISTS + not empty)
Injects orderby=meta_value_num, order=DESC, meta_key=weight
↓
Bricks executes WP_Query with modified args
↓
Results: only posts WITH a weight value, sorted highest → lowest
Other active filters (taxonomy, search) remain stacked — unaffected
Debugging Tips
Add temporary logging to trace exactly where things break:
add_filter('bricks/posts/query_vars', function($query_vars, $settings, $element_id, $element_name) {
error_log('HOOK FIRED — element: ' . $element_id);
error_log('jsfb_query_id: ' . ($settings['jsfb_query_id'] ?? 'NOT SET'));
$query_data = $_POST['query'] ?? [];
error_log('POST query: ' . print_r($query_data, true));
$sort_field = sanitize_key($query_data['_plain_query_sort_by_field'] ?? '');
error_log('sort_field extracted: ' . ($sort_field ?: 'EMPTY'));
// ... rest of your logic
error_log('Final query_vars: ' . print_r($query_vars, true));
return $query_vars;
}, 99, 4);
Check your PHP error log (usually at /wp-content/debug.log with WP_DEBUG_LOG enabled) after each filter interaction.
Summary
| What | How |
|---|---|
| Pass selected field to server | JSF Radio filter with _plain_query::sort_by_field
|
| Read the value in PHP | $_POST['query']['_plain_query_sort_by_field'] |
| Target the right loop | Check $settings['jsfb_query_id']
|
| Filter posts missing the field |
meta_query with EXISTS + != ''
|
| Sort numerically descending |
orderby=meta_value_num + meta_key + order=DESC
|
| Prevent JSF from overriding | Hook priority 99
|
| Correct hook signature | 4 params, last arg to add_filter is 4
|