How to Create a Custom Search Form with Filters in WordPress

September 10, 2025
How to Create a Custom Search Form with Filters in WordPress

Default WordPress search is simple: it looks for the query string (?s=...) across posts. You can supercharge it by adding filters (category, post type, date, meta fields, etc.) and then modifying the search query accordingly. Below is a clean, extensible approach.

What We’ll Build

  • A custom search form with filters: keyword, category, post type, date range, and a meta field (e.g., price min/max).
  • A pre_get_posts handler that reads those filters and adjusts the search results.
  • (Optional) A shortcode to place the form anywhere.

Step 1: Add the Search Form Markup (with Filters)

Place this form where you want (e.g., in a page template, a block’s “Custom HTML”, or a searchform.php file). It submits to your site’s home URL with GET params.

<form role="search" method="get" class="custom-search-form" action="<?php echo esc_url( home_url('/') ); ?>">
  <label for="cs" class="screen-reader-text">Search for:</label>
  <input type="search" id="cs" name="s" value="<?php echo isset($_GET['s']) ? esc_attr( wp_unslash($_GET['s']) ) : ''; ?>" placeholder="Search..." />

  <!-- Category filter -->
  <select name="cat" aria-label="Category">
    <option value="">All categories</option>
    <?php
      $cats = get_categories( array( 'hide_empty' => false ) );
      foreach ( $cats as $cat ) :
        $selected = ( isset($_GET['cat']) && absint($_GET['cat']) === (int) $cat->term_id ) ? 'selected' : '';
        echo '<option value="' . (int) $cat->term_id . '" ' . $selected . '>' . esc_html( $cat->name ) . '</option>';
      endforeach;
    ?>
  </select>

  <!-- Post type filter -->
  <select name="post_type" aria-label="Post type">
    <?php
      $allowed_types = array( 'any' => 'All types', 'post' => 'Posts', 'page' => 'Pages' /* add CPT slugs here e.g., 'product' */ );
      $current_type  = isset($_GET['post_type']) ? sanitize_key( $_GET['post_type'] ) : 'any';
      foreach ( $allowed_types as $slug => $label ) {
        echo '<option value="' . esc_attr($slug) . '" ' . selected( $current_type, $slug, false ) . '>' . esc_html($label) . '</option>';
      }
    ?>
  </select>

  <!-- Date range (YYYY-MM-DD) -->
  <input type="date" name="date_from" value="<?php echo isset($_GET['date_from']) ? esc_attr($_GET['date_from']) : ''; ?>" aria-label="Date from" />
  <input type="date" name="date_to" value="<?php echo isset($_GET['date_to']) ? esc_attr($_GET['date_to']) : ''; ?>" aria-label="Date to" />

  <!-- Meta filters example: price_min / price_max (for CPTs or posts with a 'price' custom field) -->
  <input type="number" name="price_min" step="0.01" placeholder="Min price" value="<?php echo isset($_GET['price_min']) ? esc_attr($_GET['price_min']) : ''; ?>" />
  <input type="number" name="price_max" step="0.01" placeholder="Max price" value="<?php echo isset($_GET['price_max']) ? esc_attr($_GET['price_max']) : ''; ?>" />

  <button type="submit">Search</button>
</form>

Tip: If you create a searchform.php in your theme, WordPress will use it for get_search_form(). You can paste the form above into that file.


Step 2: Modify the Search Query with pre_get_posts

Add the following to your theme’s functions.php (or a small site plugin). It reads the GET parameters and adjusts the main search query.

// Customize search results with filters from the custom form.
function my_filtered_search_query( $query ) {
    if ( is_admin() || ! $query->is_main_query() ) {
        return;
    }

    if ( $query->is_search() ) {
        // Post type filter
        if ( isset($_GET['post_type']) ) {
            $pt = sanitize_key( wp_unslash($_GET['post_type']) );
            // Restrict allowed options for safety
            $allowed = array( 'any', 'post', 'page', 'product' ); // add your CPT slugs
            if ( in_array( $pt, $allowed, true ) ) {
                if ( 'any' === $pt ) {
                    $query->set( 'post_type', 'any' );
                } else {
                    $query->set( 'post_type', $pt );
                }
            }
        }

        // Category filter (expects a term ID)
        if ( isset($_GET['cat']) && '' !== $_GET['cat'] ) {
            $cat_id = absint( $_GET['cat'] );
            if ( $cat_id > 0 ) {
                $query->set( 'cat', $cat_id );
            }
        }

        // Date range
        $date_query = array();
        if ( ! empty( $_GET['date_from'] ) ) {
            $date_from = sanitize_text_field( wp_unslash($_GET['date_from']) ); // YYYY-MM-DD
            $date_query['after'] = $date_from;
        }
        if ( ! empty( $_GET['date_to'] ) ) {
            $date_to = sanitize_text_field( wp_unslash($_GET['date_to']) ); // YYYY-MM-DD
            // 'inclusive' lets the end date be included.
            $date_query['before']    = $date_to;
            $date_query['inclusive'] = true;
        }
        if ( ! empty( $date_query ) ) {
            $query->set( 'date_query', array( $date_query ) );
        }

        // Meta query example: price range on meta key 'price'
        $meta_query = array();
        $meta_key   = 'price'; // change to your custom field key

        if ( isset($_GET['price_min']) && $_GET['price_min'] !== '' ) {
            $meta_query[] = array(
                'key'     => $meta_key,
                'value'   => floatval( $_GET['price_min'] ),
                'type'    => 'NUMERIC',
                'compare' => '>='
            );
        }
        if ( isset($_GET['price_max']) && $_GET['price_max'] !== '' ) {
            $meta_query[] = array(
                'key'     => $meta_key,
                'value'   => floatval( $_GET['price_max'] ),
                'type'    => 'NUMERIC',
                'compare' => '<='
            );
        }
        if ( ! empty( $meta_query ) ) {
            // If you already have meta_query, merge instead of overwrite.
            $query->set( 'meta_query', $meta_query );
        }

        // Optional: Order by relevance/date
        // $query->set( 'orderby', 'date' );
        // $query->set( 'order', 'DESC' );
    }
}
add_action( 'pre_get_posts', 'my_filtered_search_query' );

Notes

  • We only run on the main front-end search query.
  • Sanitize all user input: sanitize_key, absint, sanitize_text_field, floatval.
  • Adjust the allowed post types and the meta key to your project.

Step 3: Create/Customize search.php (Results Template)

Add a search.php in your theme (if you don’t have one) to control the search results layout. Basic example:

<?php get_header(); ?>

<main id="primary" class="site-main">

  <h1>Search Results</h1>

  <!-- Optionally display the same form again for refining filters -->
  <?php get_search_form(); ?>

  <?php if ( have_posts() ) : ?>
    <ul class="search-results">
      <?php while ( have_posts() ) : the_post(); ?>
        <li>
          <h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
          <p><?php echo wp_kses_post( wp_trim_words( get_the_excerpt(), 24 ) ); ?></p>
        </li>
      <?php endwhile; ?>
    </ul>

    <?php the_posts_pagination(); ?>

  <?php else : ?>
    <p>No results matched your filters.</p>
  <?php endif; ?>

</main>

<?php get_footer(); ?>

Optional: Provide the Form via Shortcode

Want to drop the filtered search form into any page or block? Register a shortcode:

// Shortcode: [filtered_search_form]
function my_filtered_search_form_shortcode() {
    ob_start();
    get_search_form(); // uses searchform.php if present (put the custom form markup there)
    return ob_get_clean();
}
add_shortcode( 'filtered_search_form', 'my_filtered_search_form_shortcode' );

Usage:

[filtered_search_form]

Styling (Quick Starter)

.custom-search-form {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr 1fr 1fr auto;
  gap: 10px;
  margin-bottom: 20px;
}
.custom-search-form input,
.custom-search-form select {
  padding: 8px;
}
.custom-search-form button {
  padding: 8px 12px;
}
@media (max-width: 800px) {
  .custom-search-form { grid-template-columns: 1fr; }
}

Security & Performance Tips

  • Validation: Always whitelist post types and sanitize inputs.
  • Meta queries: Add indexes for frequently queried meta keys or consider a dedicated table for heavy use.
  • Caching: For expensive queries, consider page/object caching or a fast search solution (e.g., Elasticsearch, Algolia) later.

Summary

  1. Build a custom search form with extra GET fields (category, post type, dates, meta).
  2. Use pre_get_posts to read those fields and tailor the search query.
  3. Customize search.php to present results and optionally re-display the form.
  4. (Optional) Wrap the form in a shortcode for use anywhere.

With this pattern, you can add as many filters as you need and keep your WordPress search fast, safe, and user-friendly.

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.