How to Add Admin Filters for CPTs (Taxonomy & Meta)
When a Custom Post Type (CPT) grows, the default WordPress admin list becomes hard to use.
Adding filters for taxonomy terms (categories/tags) and key meta fields (status, priority, flags)
makes content management dramatically faster—without plugins.
This article shows production-safe patterns to add admin filters for CPTs using:
restrict_manage_posts(render filter UI)parse_queryorpre_get_posts(apply filtering to the list query)
Goals and Constraints
- Target only a specific CPT admin screen
- Support taxonomy dropdown filters
- Support meta-based dropdown filters (exact match or “has value”)
- Keep queries safe and performant
- Avoid breaking other admin list tables
Step 1: Add a Taxonomy Dropdown Filter
Use restrict_manage_posts to output a dropdown on the CPT list screen.
Example: Add a taxonomy filter for the “event” CPT
<?php
add_action( 'restrict_manage_posts', function () {
global $typenow;
if ( $typenow !== 'event' ) {
return;
}
$tax = 'event_type';
$selected = isset( $_GET[ $tax ] ) ? sanitize_text_field( wp_unslash( $_GET[ $tax ] ) ) : '';
wp_dropdown_categories( array(
'show_option_all' => 'All Event Types',
'taxonomy' => $tax,
'name' => $tax,
'orderby' => 'name',
'selected' => $selected,
'hierarchical' => true,
'show_count' => true,
'hide_empty' => false,
'value_field' => 'slug',
) );
} );
This adds a taxonomy dropdown to wp-admin/edit.php?post_type=event.
Step 2: Apply the Taxonomy Filter to the Admin Query
Now convert the selected dropdown value into a taxonomy query on the list table request.
Use parse_query because it’s specifically for admin list tables.
<?php
add_filter( 'parse_query', function ( $query ) {
global $pagenow;
if ( ! is_admin() || $pagenow !== 'edit.php' ) {
return;
}
$post_type = $query->get( 'post_type' );
if ( $post_type !== 'event' ) {
return;
}
$tax = 'event_type';
if ( empty( $_GET[ $tax ] ) ) {
return;
}
$term = sanitize_text_field( wp_unslash( $_GET[ $tax ] ) );
// If you used slug in the dropdown, you can pass it directly.
$query->set( $tax, $term );
} );
Because the dropdown used value_field => slug, we can set $query->set( 'event_type', 'slug' ).
If you prefer term IDs, use field => 'term_id' and build a tax_query.
Step 3: Add Meta Filters (Dropdowns) to the Admin List
Meta filters are not built-in, but are straightforward:
render a dropdown, then add a meta_query.
Example: Filter by a meta key (status)
Assume your CPT uses a meta key event_status with values: upcoming, past.
<?php
add_action( 'restrict_manage_posts', function () {
global $typenow;
if ( $typenow !== 'event' ) {
return;
}
$key = 'event_status';
$current = isset( $_GET[ $key ] ) ? sanitize_text_field( wp_unslash( $_GET[ $key ] ) ) : '';
echo '<select name="' . esc_attr( $key ) . '">';
echo '<option value="">All Statuses</option>';
echo '<option value="upcoming"' . selected( $current, 'upcoming', false ) . '>Upcoming</option>';
echo '<option value="past"' . selected( $current, 'past', false ) . '>Past</option>';
echo '</select>';
} );
Apply the Meta Filter
<?php
add_action( 'pre_get_posts', function ( $query ) {
if ( ! is_admin() || ! $query->is_main_query() ) {
return;
}
$screen_post_type = $query->get( 'post_type' );
if ( $screen_post_type !== 'event' ) {
return;
}
$key = 'event_status';
if ( empty( $_GET[ $key ] ) ) {
return;
}
$value = sanitize_text_field( wp_unslash( $_GET[ $key ] ) );
$meta_query = (array) $query->get( 'meta_query' );
$meta_query[] = array(
'key' => $key,
'value' => $value,
'compare' => '=',
);
$query->set( 'meta_query', $meta_query );
} );
This filters the admin list to only posts matching the selected meta value.
Step 4: Add a “Has Value / Missing Value” Meta Filter
A common admin need is filtering posts that have a value set (e.g., a URL, a featured flag).
Use EXISTS / NOT EXISTS, or compare against empty values.
Example: Filter posts that have a non-empty external URL
<?php
add_action( 'restrict_manage_posts', function () {
global $typenow;
if ( $typenow !== 'event' ) {
return;
}
$param = 'has_external_url';
$current = isset( $_GET[ $param ] ) ? sanitize_text_field( wp_unslash( $_GET[ $param ] ) ) : '';
echo '<select name="' . esc_attr( $param ) . '">';
echo '<option value="">External URL: Any</option>';
echo '<option value="1"' . selected( $current, '1', false ) . '>Has URL</option>';
echo '<option value="0"' . selected( $current, '0', false ) . '>Missing URL</option>';
echo '</select>';
} );
<?php
add_action( 'pre_get_posts', function ( $query ) {
if ( ! is_admin() || ! $query->is_main_query() ) {
return;
}
if ( $query->get( 'post_type' ) !== 'event' ) {
return;
}
$param = 'has_external_url';
if ( ! isset( $_GET[ $param ] ) || $_GET[ $param ] === '' ) {
return;
}
$val = sanitize_text_field( wp_unslash( $_GET[ $param ] ) );
$meta_query = (array) $query->get( 'meta_query' );
if ( $val === '1' ) {
$meta_query[] = array(
'key' => 'external_url',
'value' => '',
'compare' => '!=',
);
} elseif ( $val === '0' ) {
$meta_query[] = array(
'relation' => 'OR',
array(
'key' => 'external_url',
'compare' => 'NOT EXISTS',
),
array(
'key' => 'external_url',
'value' => '',
'compare' => '=',
),
);
}
$query->set( 'meta_query', $meta_query );
} );
This pattern is practical when your meta field may be missing entirely or stored as an empty string.
Step 5: Filter + Sort Together (Example: Priority Meta Ordering)
Admin filters often pair with sorting.
You can sort by a numeric meta key when needed.
<?php
add_action( 'pre_get_posts', function ( $query ) {
if ( ! is_admin() || ! $query->is_main_query() ) {
return;
}
if ( $query->get( 'post_type' ) !== 'event' ) {
return;
}
// Example: if a custom GET param triggers sorting.
$param = 'sort_priority';
if ( empty( $_GET[ $param ] ) ) {
return;
}
$query->set( 'meta_key', 'priority' );
$query->set( 'orderby', 'meta_value_num' );
$query->set( 'order', 'DESC' );
} );
Security and Performance Notes
- Always sanitize
$_GETvalues (usewp_unslash()+sanitize_text_field()) - Use exact comparisons (
=) where possible; avoidLIKEin admin list filters - Meta queries can be expensive on large datasets; filter only on high-value fields
- Scope changes to the intended CPT screen only
Common Mistakes
- Adding filters on every post type screen (no scoping)
- Using
pre_get_postswithout checkingis_main_query() - Assuming meta keys always exist (missing meta breaks logic)
- Not preserving selected dropdown values on reload
Summary
- Use
restrict_manage_poststo render taxonomy + meta dropdown filters - Use
parse_queryfor taxonomy params andpre_get_postsfor meta queries - Sanitize all
$_GETinput and scope to a specific CPT admin screen - Support “has value / missing value” filters with
NOT EXISTSand empty comparisons - Optionally combine filters with custom sorting
With a few well-scoped hooks, you can turn WordPress admin CPT lists into a fast, practical content management UI—
without relying on plugins.
🎨 Want to learn more? Visit our WordPress Customization Hub for tips and advanced techniques.