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_postshandler 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
- Build a custom search form with extra GET fields (category, post type, dates, meta).
- Use
pre_get_poststo read those fields and tailor the search query. - Customize
search.phpto present results and optionally re-display the form. - (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.
🎨 Want to learn more? Visit our WordPress Customization Hub for tips and advanced techniques.