WordPress plugins often need to display large datasets—orders, user submissions, or custom records—in the admin dashboard. Without wordPress admin table filters and search, users waste time scrolling through irrelevant data. By adding these features, you empower admins to:
- Find specific entries instantly (e.g., orders by status).
- Filter data by date, category, or custom criteria.
- Improve workflow efficiency with paginated results.
In this guide, you’ll learn two methods to implement filters and search:
- Using WordPress built-in WP_List_Table class (recommended for standard tables).
- Building a custom HTML table (for advanced layouts).
Requirements
- Basic knowledge of WordPress plugin development.
- Familiarity with PHP, SQL, and WordPress hooks.
- Access to a custom database table (e.g., created via dbDelta()).
Method 1: Using WP_List_Table (Beginner-Friendly)
WP_List_Table handles pagination, sorting, and bulk actions out-of-the-box. Here’s how to extend it for filters and search.
Step 1: Create a Custom Class Extending WP_List_Table
if (!class_exists('WP_List_Table')) {
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}
class Custom_Data_Table extends WP_List_Table {
// Define columns, prepare items, and more
}
Step 2: Define Columns and Data Preparation
public function prepare_items() {
global $wpdb;
// Define columns
$columns = $this->get_columns();
$hidden = array();
$sortable = $this->get_sortable_columns();
// Fetch data
$search = isset($_REQUEST['s']) ? sanitize_text_field($_REQUEST['s']) : '';
$status_filter = isset($_REQUEST['status']) ? sanitize_text_field($_REQUEST['status']) : '';
$query = "SELECT * FROM {$wpdb->prefix}custom_table";
$where = array();
// Add search
if ($search) {
$where[] = $wpdb->prepare("(name LIKE %s OR email LIKE %s)", "%$search%", "%$search%");
}
// Add status filter
if ($status_filter) {
$where[] = $wpdb->prepare("status = %s", $status_filter);
}
if (!empty($where)) {
$query .= ' WHERE ' . implode(' AND ', $where);
}
// Pagination
$total_items = $wpdb->query($query);
$per_page = 20;
$current_page = $this->get_pagenum();
$this->set_pagination_args(array(
'total_items' => $total_items,
'per_page' => $per_page,
));
$query .= $wpdb->prepare(" LIMIT %d, %d", ($current_page - 1) * $per_page, $per_page);
$this->items = $wpdb->get_results($query, ARRAY_A);
}
Step 3: Add Filters and Search Box
Override extra_tablenav() to add dropdown filters:
public function extra_tablenav($which) {
if ($which === 'top') {
$statuses = array('pending', 'completed', 'cancelled');
echo '<div class="alignleft actions">';
echo '<select name="status" id="status-filter">';
echo '<option value="">All Statuses</option>';
foreach ($statuses as $status) {
$selected = isset($_REQUEST['status']) && $_REQUEST['status'] === $status ? 'selected' : '';
echo "<option value='$status' $selected>" . ucfirst($status) . "</option>";
}
echo '</select>';
submit_button('Filter', 'button', 'filter_action', false);
echo '</div>';
}
}
Step 4: Display the Table
In your plugin’s admin page:
function display_custom_table() {
$table = new Custom_Data_Table();
$table->prepare_items();
echo '<form method="get">';
echo '<input type="hidden" name="page" value="your-plugin-page" />';
$table->search_box('Search', 'search_id');
$table->display();
echo '</form>';
}
Method 2: Custom HTML Table (Advanced)
For full control over design and functionality, build a table from scratch.
Step 1: Create the HTML Form and Table Structure
function display_custom_table() {
echo '<form method="get" action="">';
echo '<input type="hidden" name="page" value="your-plugin-page" />';
// Search Box
echo '<p class="search-box">';
echo '<input type="search" name="s" value="' . esc_attr(isset($_GET['s']) ? $_GET['s'] : '') . '" />';
submit_button('Search', 'button', false, false);
echo '</p>';
// Status Filter
echo '<select name="status">';
echo '<option value="">All Statuses</option>';
foreach (array('pending', 'completed') as $status) {
$selected = isset($_GET['status']) && $_GET['status'] === $status ? 'selected' : '';
echo "<option value='$status' $selected>" . ucfirst($status) . "</option>";
}
echo '</select>';
submit_button('Filter', 'button', false, false);
// Display table
echo '<table class="wp-list-table widefat fixed striped">';
echo '<thead><tr><th>ID</th><th>Name</th><th>Status</th></tr></thead>';
echo '<tbody>';
// Populate data here
echo '</tbody></table>';
// Pagination
echo '<div class="tablenav bottom">';
echo '<div class="tablenav-pages">';
echo paginate_links(array(
'base' => add_query_arg('paged', '%#%'),
'format' => '',
'prev_text' => __('« Previous'),
'next_text' => __('Next »'),
'total' => $total_pages,
'current' => $current_page,
));
echo '</div></div>';
echo '</form>';
}
Step 2: Query Data Based on Filters
global $wpdb;
$per_page = 20;
$current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
$offset = ($current_page - 1) * $per_page;
$where = array();
$params = array();
// Handle search
if (!empty($_GET['s'])) {
$search = sanitize_text_field($_GET['s']);
$where[] = "(name LIKE %s OR email LIKE %s)";
$params[] = "%$search%";
$params[] = "%$search%";
}
// Handle status filter
if (!empty($_GET['status'])) {
$status = sanitize_text_field($_GET['status']);
$where[] = "status = %s";
$params[] = $status;
}
// Build query
$query = "SELECT * FROM {$wpdb->prefix}custom_table";
if (!empty($where)) {
$query .= ' WHERE ' . implode(' AND ', $where);
}
$query .= $wpdb->prepare(" LIMIT %d OFFSET %d", $per_page, $offset);
$results = $wpdb->get_results($query, ARRAY_A);
Step 3: Populate Table Data
foreach ($results as $row) {
echo "<tr>";
echo "<td>{$row['id']}</td>";
echo "<td>{$row['name']}</td>";
echo "<td>{$row['status']}</td>";
echo "</tr>";
}
Security Best Practices
- Sanitize Inputs: Always sanitize $_GET/$_REQUEST values with sanitize_text_field().
- Use Nonces: Add nonces to forms to prevent CSRF attacks.
- Escape Outputs: Use esc_html() or esc_attr() when rendering data.
Pagination with Search Parameters
When generating pagination links, include current filter/search values:
$pagination_args = array(
'base' => add_query_arg('paged', '%#%'),
'format' => '',
'prev_text' => '« Previous',
'next_text' => 'Next »',
'total' => $total_pages,
'current' => $current_page,
'add_args' => array(
's' => isset($_GET['s']) ? $_GET['s'] : '',
'status' => isset($_GET['status']) ? $_GET['status'] : '',
),
);
echo paginate_links($pagination_args);
Frequently Asked Questions
Conclusion
Integrating filters and search into your plugin’s admin table revolutionizes how users manage data, turning overwhelming datasets into streamlined workflows. Whether opting for the built-in efficiency of WP_List_Table or crafting a custom HTML table for tailored designs, the core principles remain consistent: securely capturing user inputs, dynamically adjusting database queries based on those inputs, and ensuring pagination retains applied filters. These steps not only enhance functionality but also maintain a seamless experience as users navigate through pages of results.
For those looking to deepen their expertise, resources like WordPress Codex WP_List_Table documentation or SQL optimization guides offer valuable insights. By mastering these techniques, you’ll deliver an intuitive and powerful admin interface, setting your plugin apart in the competitive WordPress ecosystem.
About the writer
Hassan Tahir wrote this article, drawing on his experience to clarify WordPress concepts and enhance developer understanding. Through his work, he aims to help both beginners and professionals refine their skills and tackle WordPress projects with greater confidence.