In one of the previous posts, we’ve investigated how to add custom filters to the WooCommerce orders table. Today’s topic is quite similar, but this time we’re going to investigate how to add custom filters to the WordPress users table.
This question is especially hot for website owners with at least basic membership features and custom user fields. WordPress uses are not a post type; a user is an entirely separate entity with its own table in the database. It means that we won’t be able to use the same pre_get_posts hook we’ve used before.
Luckily, the developers of the WordPress core foreseen that some users would like to create their own filters and modify the database queries for users too.
Default behavior
The WordPress users table includes only one filter by default: you can see it right above the actions bar.
You can filter your users by their role using those “buttons”, as you might have guessed.
We have a search box here too. By default, it allows you to search users by their username and email.
Not too many features here, comparing to the WooCommerce orders table, right?
Ready-to-use plugins
There’s a lot of WordPress membership plugins. Some of them allow website owners to create their own user filters. But if the only thing that you want is to just add a few custom filters to the users’ table, these membership plugins would be an overkill solution for you. In the future, I’ll write a series of posts about the best WordPress membership plugins. But this time, I’ll skip this topic and go straight to the solution.
The only plugin I have found that looks like exactly what you may need is the Admin Columns plugin. I haven’t worked with this plugin before, but based on the screenshots and demo videos they show, it looks really nice if you want to make your WordPress admin tables more customizable. I also need to note that this plugin is not free. When I wrote this post, the cheapest plan costs $89 per year for one website.
Let me know in the comments if you want me to write a full review of this plugin.
Custom code
If you want to build something simple or you don’t want to spend money on premium plugins, this part of the post is for you.
This time I want to make our goal a bit more complicated. In the post about the custom orders’ filters, we’ve added a custom checkbox that allowed us to filter the orders by the is_first_order meta field. In this post, we’re going to create a custom select box with dynamic options.
What do I mean by “dynamic”? Well, you can set a select’s options statically in HTML. In this case, you’ll have to update these options whenever a new option is added, or an old option is deleted. Instead, we’re going to create our options dynamically: we’ll receive them from the database and then display them using PHP.
Adding a new field
Using ACF, I’ve created a new custom field for users: Favorite CMS. This is a select field with the next options:
- WordPress
- Shopify
- Joomla
- Wix
- Squarespace
- Other
If you’re wondering why I have chosen exactly these CMS: W3Techs statistics show that those are the most widely used CMS in the world.
Here’s how this field looks in the admin area when I open a user’s editor page:
Adding a new table column
Now we need to show this data somewhere in the table. Let’s add a new column and call it just like the field.
add_filter( 'manage_users_columns', 'register_favorite_cms_column' );
function register_favorite_cms_column( $columns ) {
$columns['favorite_cms'] = 'Favorite CMS';
return $columns;
}
add_filter( 'manage_users_custom_column', 'render_users_favorite_cms', 10, 3 );
function render_users_favorite_cms( $output, $column_name, $user_id ) {
if ( 'favorite_cms' === $column_name ) {
// Don't forget to escape your output
$output = esc_html( get_user_meta( $user_id, 'favorite_cms', true ) );
}
return $output;
}
The manage_users_columns hook allows us to register a new column and the manage_users_custom_column hook allows us to render our custom column’s value retrieving it from a user’s meta data.
Let’s see how the users’ table looks now after we’ve added this code:
Looks pretty good, just like a part of the WordPress core. Now let’s add our custom filter option that will allow us to filter users by their favorite CMS.
Getting familiar with the pre_get_users hook
As I said at the beginning of this post, users are not a post type; they are a completely independent entity. In case if you would want to modify a WP_User_Query instance, the developers of the WordPress core created a special hook that works pretty similar to what we saw with the pre_get_posts hook. As you might have guessed, this hook is called pre_get_users.
As the WordPress documentation says, the pre_get_users hook “fires before the WP_User_Query has been parsed”. It means that we can modify the meta_query variable of this query to filter our users by a meta field’s value.
Once again, be careful: before modifying any query variables, we must make sure that we modify only the request we need. Don’t forget to check that you modify the exact query you need. Otherwise, your hook might break the work of other parts of your website.
Here’s an example of the code that will work on the users’ table page in the WordPress dashboard:
add_action( 'pre_get_users', 'filter_users_by_favorite_cms', 99, 1 );
function filter_users_by_favorite_cms( $query ) {
// This condition allows us to make sure that we won't modify any query that came from the frontend
if ( ! is_admin() ) {
return;
}
global $pagenow;
// This condition allows us to make sure that we're modifying a query that fires on the wp-admin/users.php page
if ( 'users.php' === $pagenow ) {
// Our filtering logic goes here
}
return;
}
How to use the meta_query variable to filter users
Since a user’s favorite CMS is stored as a custom meta field, we’ll need to use the meta_query variable of the WP_User_Query instance to filter our users.
The meta_query variable always expects an array of arrays, even if we have only one condition. Let’s add it to our previous code snippet:
add_action( 'pre_get_users', 'filter_users_by_favorite_cms', 99, 1 );
function filter_users_by_favorite_cms( $query ) {
// This condition allows us to make sure that we won't modify any query that came from the frontend
if ( ! is_admin() ) {
return;
}
global $pagenow;
// This condition allows us to make sure that we're modifying a query that fires on the wp-admin/users.php page
if ( 'users.php' === $pagenow ) {
$meta_query = array(
array(
'key' => 'favorite_cms',
'value' => 'WordPress',
'compare' => '='
)
);
$query->set( 'meta_query', $meta_query );
}
return;
}
The code above means that in the users’ table we want to show only the users whose favorite_cms meta field’s value is set to “WordPress”. Let’s see if this code really works:
Great! Works just like expected: we see the WordPress lovers in our table only.
The problem is that this code is not dynamic: if we want to see the Joomla lovers only, we’ll need to modify the code. Let’s make this filtering feature dynamic from the frontend and add a custom select box to easily filter our users straight from the table.
How to add custom inputs before the users table?
The hook we can use to show our custom filter options is manage_users_extra_tablenav. This hook triggers right after the actions bar. I highlighted the action bar on the screenshot below to make this description more visual.
If you need to add more than 2 custom filtering options, you can wrap them into a container block and show them separately under the actions bar. Here’s a sample of the code you might write to implement something like this:
<?php
add_action( 'manage_users_extra_tablenav', 'render_custom_filter_options' );
function render_custom_filter_options() {
?>
<form method="GET">
<div class="custom-user-filters">
<select>
<option>Favorite CMS...</option>
</select>
<select>
<option>Favorite food...</option>
</select>
<input type="submit" class="button action" value="Filter">
</div>
</form>
<?php
}
Note that we wrap our container into a form element. If you won’t wrap your custom select boxes into a separate form container, their value would be ignored by the default WordPress form.
If you style this container a little bit, you should achieve something like this:
Let’s add our custom select box
Since we need to add only one select box, there’s no need to create a special container for it. Let’s keep everything simple and render our select box near the action bar. Here’s the code I will use:
<?php
add_action( 'manage_users_extra_tablenav', 'render_custom_filter_options' );
function render_custom_filter_options() {
?>
<select name="favorite_cms">
<option value="0">Favorite CMS...</option>
<!-- We'll add all possible options of this field later -->
</select>
<input type="submit" class="button action" value="Filter">
<?php
}
Let’s look at what we’ve got in our users’ table after we’ve added this code.
Nice, looks very natural. Now we need to load all possible options of this select box as its options.
How to retrieve all possible options of an ACF select field?
There’s a special function in the ACF plugin called get_field_object. We won’t dive too deep into the specifics of this function this time, but the main thing you need to know about it is that this function’s result contains all possible options of a select field.
As a first argument, this function accepts the name or the key of a field you want to retrieve. I personally recommend you to use the key of the field since you can have more than one ACF field with the same name. You can see a field’s key by opening your field group in your browser’s developer tools:
Let’s use this function to retrieve the options of our select box dynamically. Here’s the code I will use:
<?php
add_action( 'manage_users_extra_tablenav', 'render_custom_filter_options' );
function render_custom_filter_options() {
$favorite_cms_field = get_field_object( 'field_614a260146283' );
$options = $favorite_cms_field['choices'];
?>
<form method="GET">
<select name="favorite_cms">
<option value="0">Favorite CMS...</option>
<?php foreach ( $options as $value => $label ): ?>
<option value="<?php echo esc_attr( $value ); ?>"><?php echo esc_html( $label ); ?></option>
<?php endforeach; ?>
</select>
<input type="submit" class="button" value="Filter">
</form>
<?php
}
Now, if I open the users’ table, I can see that all options of this field were dynamically loaded as our select’s options:
Great! It means that we’re almost done. Now we need to get back to our pre_get_users hook and make it dynamic too. Let’s rewrite our previous code a little bit:
add_action( 'pre_get_users', 'filter_users_by_favorite_cms', 99, 1 );
function filter_users_by_favorite_cms( $query ) {
// This condition allows us to make sure that we won't modify any query that came from the frontend
if ( ! is_admin() ) {
return;
}
global $pagenow;
// This condition allows us to make sure that we're modifying a query that fires on the wp-admin/users.php page
if ( 'users.php' === $pagenow ) {
// Let's check if our filter has been used
if ( isset( $_GET['favorite_cms'] ) && $_GET['favorite_cms'] !== '0' ) {
$meta_query = array(
array(
'key' => 'favorite_cms',
'value' => $_GET['favorite_cms'],
'compare' => '='
)
);
$query->set( 'meta_query', $meta_query );
}
}
return;
}
That’s all. Now, if someone would choose an option and click the “Filter” button, a user could see the list of filtered users by their favorite CMS.
Final code
<?php
add_filter( 'manage_users_columns', 'register_favorite_cms_column' );
function register_favorite_cms_column( $columns ) {
$columns['favorite_cms'] = 'Favorite CMS';
return $columns;
}
add_filter( 'manage_users_custom_column', 'render_users_favorite_cms', 10, 3 );
function render_users_favorite_cms( $output, $column_name, $user_id ) {
if ( 'favorite_cms' === $column_name ) {
// Don't forget to escape your output
$output = esc_html( get_user_meta( $user_id, 'favorite_cms', true ) );
}
return $output;
}
add_action( 'manage_users_extra_tablenav', 'render_custom_filter_options' );
function render_custom_filter_options() {
$favorite_cms_field = get_field_object( 'field_614a260146283' );
$options = $favorite_cms_field['choices'];
?>
<form method="GET">
<select name="favorite_cms">
<option value="0">Favorite CMS...</option>
<?php foreach ( $options as $value => $label ): ?>
<option value="<?php echo esc_attr( $value ); ?>"><?php echo esc_html( $label ); ?></option>
<?php endforeach; ?>
</select>
<input type="submit" class="button" value="Filter">
</form>
<?php
}
add_action( 'pre_get_users', 'filter_users_by_favorite_cms', 99, 1 );
function filter_users_by_favorite_cms( $query ) {
// This condition allows us to make sure that we won't modify any query that came from the frontend
if ( ! is_admin() ) {
return;
}
global $pagenow;
// This condition allows us to make sure that we're modifying a query that fires on the wp-admin/users.php page
if ( 'users.php' === $pagenow ) {
// Let's check if our filter has been used
if ( isset( $_GET['favorite_cms'] ) && $_GET['favorite_cms'] !== '0' ) {
$meta_query = array(
array(
'key' => 'favorite_cms',
'value' => $_GET['favorite_cms'],
'compare' => '='
)
);
$query->set( 'meta_query', $meta_query );
}
}
return;
}
You can use this code inside of your theme’s functions.php file, or you can create a plugin and use this code there. Feel free to experiment with this code and add new filter options there.
Let me know in the comments if this post was helpful for you or if you’ve got any questions about its content. Thank you for your attention; see you in the next one!
Hi Artemy. Thanks for taking the time and writing this post. Exactly what I was looking for and your example was simple and clear!
Hi Marco, thank you for your comment! I’m glad you found my post useful 🙂
Hi Artemy, thank you for your article. However I’m looking for a way to display user’s comment count in the new column. Is there a way to do it? I would appreciate your help.
Hi Roman! Thank you for your comment. Yes, it’s possible. Take a look at this answer:
https://wordpress.stackexchange.com/a/358991/191232
Thank you. I was just looking to display a custom field in the WP Users admin table, and this worked perfectly.
Thanks for your comment, always happy to help 🙂
Great guide. Enough explanation that I was able to adapt it to other plugins and uses.
Quick note if its helpful for anyone: Including the form html tags broke the other filter options on the top of the users table for me. Instead I rewrote form to just have an input and submit option(since the hook inserts it into the existing form structure)
And the made the rest of the tables filter functions work for me.
Thanks again!
Thank you for your comment, Scott, I’m glad that my post was helpful for you!