How to Add Admin Filters for CPTs (Taxonomy & Meta)

January 9, 2026
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_query or pre_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 $_GET values (use wp_unslash() + sanitize_text_field())
  • Use exact comparisons (=) where possible; avoid LIKE in 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_posts without checking is_main_query()
  • Assuming meta keys always exist (missing meta breaks logic)
  • Not preserving selected dropdown values on reload

Summary

  • Use restrict_manage_posts to render taxonomy + meta dropdown filters
  • Use parse_query for taxonomy params and pre_get_posts for meta queries
  • Sanitize all $_GET input and scope to a specific CPT admin screen
  • Support “has value / missing value” filters with NOT EXISTS and 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.

Avatar

Written by

satoshi

I’ve been building and customizing WordPress themes for over 10 years. In my free time, you’ll probably find me enjoying a good football match.