How to Make WordPress Themes & Plugins User-Friendly

Building a great WordPress theme or plugin is as much about making it easy to use as it is about functionality and optimized code. Every time users activate a theme or plugin and struggle to find their way to and around its settings, somewhere in the world a kitten suffers. It would be nice if we put an end to that, right?
Luckily, all you need to do to make your plugins and themes more usable is take advantage of WordPress’ built-in functionality. “Reinventing the wheel” is not on the list of skills required for this. Let’s take a look at some techniques that will help users find their way around and take frustration out of using your plugins and themes.
Table of contents
Adding Custom Admin Pointers
Introduced in WordPress 3.3, WordPress admin pointers are the most aggressive way of grabbing users’ attention, so don’t go pointer crazy. Still, if there’s something you absolutely must tell folks who just installed/upgraded your theme or plugin, WordPress admin pointers are the way to go.
They are very easy to use but poorly documented at WordPress Codex website. Here’s a quick breakdown of how pointers work, followed by an example that adds a pointer next to Settings menu.
- A theme or plugin can register new pointers, assigning a unique ID to each one
- Pointers are shown to users until they click “Dismiss” link
- When that happens, pointer ID is added to user’s
dismissed_wp_pointers
meta key and pointer is no longer shown
And the example, as promised:
/**
* Adds a simple WordPress pointer to Settings menu
*/
function thsp_enqueue_pointer_script_style( $hook_suffix ) {
// Assume pointer shouldn't be shown
$enqueue_pointer_script_style = false;
// Get array list of dismissed pointers for current user and convert it to array
$dismissed_pointers = explode( ',', get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true ) );
// Check if our pointer is not among dismissed ones
if( !in_array( 'thsp_settings_pointer', $dismissed_pointers ) ) {
$enqueue_pointer_script_style = true;
// Add footer scripts using callback function
add_action( 'admin_print_footer_scripts', 'thsp_pointer_print_scripts' );
}
// Enqueue pointer CSS and JS files, if needed
if( $enqueue_pointer_script_style ) {
wp_enqueue_style( 'wp-pointer' );
wp_enqueue_script( 'wp-pointer' );
}
}
add_action( 'admin_enqueue_scripts', 'thsp_enqueue_pointer_script_style' );
function thsp_pointer_print_scripts() {
$pointer_content = "<h3>Stop looking for it, it is right here!</h3>";
$pointer_content .= "<p>If you ever activated a plugin, then had no idea where its settings page is, raise your hand.</p>"; ?>
<script type="text/javascript">
//<![CDATA[
jQuery(document).ready( function($) {
$('#menu-settings').pointer({
content: '<?php echo $pointer_content; ?>',
position: {
edge: 'left', // arrow direction
align: 'center' // vertical alignment
},
pointerWidth: 350,
close: function() {
$.post( ajaxurl, {
pointer: 'thsp_settings_pointer', // pointer ID
action: 'dismiss-wp-pointer'
});
}
}).pointer('open');
});
//]]>
</script>
<?php
} ?>
Which will result in something like this:

This was just a simple one, if you want to learn more about WordPress admin pointers check out this article on Integrating With WordPress’ UI: Admin Pointers.
Showing Custom Admin Notices
If admin pointers were a guy holding a big arrow sign in front of a shop, admin notices would be that same guy handing out flyers at a remote location. Not exactly dragging you in, but still grabbing attention. Of course, you don’t want to show notices all the time, so you should either make them dismissible or place them inside a conditional function. Here’s an example:
/**
* Add admin notices
*/
function thsp_admin_notices() {
global $current_user;
$userid = $current_user->ID;
global $pagenow;
// This notice will only be shown in General Settings page
if ( $pagenow == 'options-general.php' ) {
echo '<div class="updated">
<p>This is an "updated" notice.</p>
</div>';
}
// Only show this notice if user hasn't already dismissed it
// Take a good look at "Dismiss" link href attribute
if ( !get_user_meta( $userid, 'ignore_sample_error_notice' ) ) {
echo '<div class="error">
<p>This is an "error" notice. <a href="?dismiss_me=yes">Dismiss</a>.</p>
</div>';
}
}
add_action( 'admin_notices', 'thsp_admin_notices' );
First notice in this example will only be shown in General Settings page. Second one is only shown to users who haven’t dismissed it. As you can see, it checks for current user’s ignore_sample_error_notice user meta field and is displayed only if that field is empty. So how do we add user meta field when they click “Dismiss”? Easy:
/**
* Add user meta value when Dismiss link is clicked
*/
function thsp_dismiss_admin_notice() {
global $current_user;
$userid = $current_user->ID;
// If "Dismiss" link has been clicked, user meta field is added
if ( isset( $_GET['dismiss_me'] ) && 'yes' == $_GET['dismiss_me'] ) {
add_user_meta( $userid, 'ignore_sample_error_notice', 'yes', true );
}
}
add_action( 'admin_init', 'thsp_dismiss_admin_notice' );
We’re hooking into admin_init
action and checking if dismiss_me
GET parameter has been set. Since href attribute for our “Dismiss” link is ?dismiss_me=yes
once a user clicks it, user meta field is added and it’s good bye notice.
Adding Contextual Help

Imagine a world in which any documentation you need is at your fingertips, exactly and only when you need it. Now let’s make it happen.
Contextual help not only makes this possible, but also very easy. Let’s register a settings page for our plugin so we can add some contextual help to it.
/**
* Add settings page, under Settings menu
*/
function thsp_add_settings_page() {
global $thsp_settings_page;
$thsp_settings_page = add_options_page(
'Our Settings Page',
'Our Settings Page',
'manage_options',
'thsp_settings_page',
'thsp_show_settings_page'
);
// Check if WP version is 3.3 or higher, add contextual help
global $wp_version;
if ( version_compare( $wp_version, '3.3') >= 0 ) {
add_action( 'load-' . $thsp_settings_page, 'thsp_add_help_tabs' );
}
}
add_action( 'admin_menu', 'thsp_add_settings_page' );
We won’t be dealing with settings page call back function – thsp_show_settings_page
, since it falls out of scope of this blog post. If you need to learn about WordPress settings pages Tom McFarlin of Wptuts+ has got you covered. Anyway, the bit of code we really want to take a deeper look at is this:
// Check if WP version is 3.3 or higher, add contextual help
global $wp_version;
if ( version_compare( $wp_version, '3.3') >= 0 ) {
add_action( 'load-' . $thsp_settings_page, 'thsp_add_help_tabs' );
}
WordPress 3.3 or higher is required, since we’ll be using add_help_tab function to add contextual help tabs. Notice how hook used in add_action has a variable in it – 'load-' . $thsp_settings_page
? This makes sure thsp_add_help_tabs
function only gets hooked in settings page we just registered. Brilliant.
Now, here’s the function that adds help tabs:
/**
* Callback function for contextual help, requires WP 3.3
*/
function thsp_add_help_tabs () {
global $wp_version;
if ( version_compare( $wp_version, '3.3') >= 0 ) {
global $thsp_settings_page;
$screen = get_current_screen();
// Check if current screen is settings page we registered
// Don't add help tab if it's not
if ( $screen->id != $thsp_settings_page )
return;
// Add help tabs
$screen->add_help_tab( array(
'id' => 'thsp_first_tab',
'title' => __( 'First tab', 'thsp_contextual_help' ),
'content' => __( '
<p>Yeah, you can even embed videos, nice!</p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/RBA-lH2a6E8" frameborder="0" allowfullscreen></iframe>
', 'thsp_contextual_help'
),
) );
$screen->add_help_tab( array(
'id' => 'thsp_second_tab',
'title' => __( 'Second tab', 'thsp_contextual_help' ),
'content' => __( '
<p>I\'m just a second tab that no one will ever click.</p>
', 'thsp_contextual_help'
),
) );
// Set help sidebar
$screen->set_help_sidebar(
'
<ul>
<li><a href="https://thematosoup.com">' . __( 'Our website', 'ts-fab' ) . '</a></li>
<li><a href="https://twitter.com/#!/thematosoup">Twitter</a></li>
<li><a href="https://www.facebook.com/ThematoSoup">Facebook</a></li>
<li><a href="https://plus.google.com/104360438826479763912">Google+</a></li>
<li><a href="https://www.linkedin.com/company/thematosoup">LinkedIn</a></li>
</ul>
'
);
}
}
We just need to check if WordPress version is 3.3 or higher, make sure we’re on the correct page and add help tab using add_help_tab function and help sidebar using set_help_sidebar. Everything else is plain HTML.
If there are any downsides to contextual help it’s that majority of WordPress users aren’t even aware of it (Screen Options, too). So, maybe a pointer, to make sure they don’t miss it?
Adding WordPress Admin Bar Links

These are very handy, especially for logged in users browsing front-end of their sites. They provide one click access to most important dashboard functions and if you feel like your theme or plugin deserves a spot in WordPress Admin Bar, this is how easy it is to do it:
/**
* Admin bar customization
*/
function thsp_admin_bar_links() {
global $wp_admin_bar;
// Adds a new submenu to an existing to level admin bar link
$wp_admin_bar->add_menu( array(
'parent' => 'new-content',
'id' => 'install_plugin',
'title' => __( 'Plugin', 'thsp_admin_bar' ),
'href' => admin_url( 'plugin-install.php' )
) );
// Adds a new top level admin bar link and a submenu to it
$wp_admin_bar->add_menu( array(
'parent' => false,
'id' => 'custom_top_level',
'title' => __( 'Top Level', 'thsp_admin_bar' ),
'href' => '#'
) );
$wp_admin_bar->add_menu( array(
'parent' => 'custom_top_level',
'id' => 'custom_sub_menu',
'title' => __( 'Sub Menu', 'thsp_admin_bar' ),
'href' => '#'
) );
// Removes an existing top level admin bar link
$wp_admin_bar->remove_menu( 'comments' );
}
add_action( 'wp_before_admin_bar_render', 'thsp_admin_bar_links' );
We’re using wp_before_admin_bar_render
action hook to modify $wp_admin_bar object before it gets rendered. Example above adds a submenu to an existing top level link (New), a new top level link with another link nested inside it (Top Level, Sub Menu) and removes an existing top level link (Comments).
Adding Plugin Action and Meta Links

Dashboard Plugins screen shows a list of all installed plugins. You can see each plugin’s name, description, version, links to author and plugin website and action links – a combination of Activate, Deactivate, Edit, Delete, depending on whether plugin is activated or not.
For some plugins that’s good enough. But if your plugin has a settings page, I’d like to hear one good reason not to add an action link to it, especially if it’s as simple as this:
/**
* Add action links in Plugins table
*/
function thsp_plugin_action_links( $links ) {
return array_merge(
array(
'settings' => '<a href="' . admin_url( 'tools.php?page=our-settings-page.php' ) . '">' . __( 'Settings', 'ts-fab' ) . '</a>'
),
$links
);
}
add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), 'thsp_plugin_action_links' );
You should add this code to your plugin’s main file (plugin-name.php) so proper hook can be used. For example, if your plugin’s main file really is plugin-name.php, ‘plugin_action_links_plugin-name’ filter hook will be used, making sure action links are added only for your plugin. Another one of those magic WordPress moments.
Plugin row meta links are slightly different. Hook name is not dynamic, so you need to pass two arguments to your custom function, an array of existing links and current plugin links are being processed for:
/**
* Add meta links in Plugins table
*/
function thsp_plugin_meta_links( $links, $file ) {
$plugin = plugin_basename(__FILE__);
if ( $file == $plugin ) {
return array_merge(
$links,
array( '<a href="https://twitter.com/thematosoup">Follow us on Twitter</a>' )
);
}
return $links;
}
add_filter( 'plugin_row_meta', 'thsp_plugin_meta_links', 10, 2 );
It’s up to you to select which links to show where, so if you never dealt with plugin action and meta links before, check list of your installed plugins to see how other developers are doing it.
Conclusion
Some smart planning, common sense and WordPress built-in functionality can get you a long way, but as with everything else in life, moderation is key. Too many unneeded admin pointers or admin bar links can be frustrating.
You don’t have to be a usability expert to make usable WordPress products!
Thanks so much Slobodan for joining our little community here at WPExplorer 😉 You are a passionate WordPress developer and your skills are superb!
I’ll be the first to admit that I can make my plugins/themes a bit more user-friendly and will definitely put your tips to use.
Thanks for sharing! We look forward to more awesome posts!
AJ,
I could hardly feel more welcome than I do right now 🙂
It’s a great community to be a part of!
This is a great post! That was missing, thanks so much for putting in this post these useful techniques in one single place!
Thanks Remi! I hope many developers will find it useful.
Congrats bro 🙂
Thanks, Ranko 🙂
Another outstanding tutorial. Thank you!
Thanks Gregg, glad to hear you found it useful!
Great article. Thanks. I just regreat that everything is not i18n ready. Nowadays it should be No optionnal. For the Rest i learned a lot of nice tips. A must read and use
FxB,
I agree and I should’ve mentioned that.
Just for purpose of this tutorial, it wouldn’t make a difference if “Our Settings Page” was translation ready, but it could do damage if someone just copies the code.
Thanks for your comment.
It’s exactly for that, to give the best copy/paste possible to future dev especialy when the post is going to be used as a référence cause it deserved it
Yes, great point then, I’ll make sure code is edited, so there are no untranslatable strings. Thanks for spotting that.
Wow, this is awesome Slobodan! I was thinking of using this on one of my themes for clients, hope you share more tips about WP.
Sanjay,
Thanks for commenting.
That’s the plan 🙂
Great post! It helps me to increase usability of my themes (especially WP Pointers)
Thanks )
Glad to hear that! Thanks for commenting.
I think the contextual help is useful, but not much user friendly. Indeed, I see people rarely use it to find helpful information.
A couple of questions about Admin Pointers: first off, in the pointer, is it default to have the pointer box point to the Settings list, or how does WordPress know where to point the pointer? How can it be pointed to a plugin-added menu / sub-menu list?
As for the Meta Links, I’ve copied over the code directly, and all that happens is that every meta link for all plugins disappears. Any idea what is going on? I already used the code for the action links but I’d like to include additional meta links.
Last: you’ve got a couple of “>= 0” statements in your Contextual Help code.
For admin pointers, if you check Javascript code, there’s this in it:
So that’s why it’s next to Settings menu, but it can be anywhere on the page.
Plugin meta links – the code should work, if placed in main plugin file.
Thanks for catching >=0 errors!
Well, I searched how a couple of other plugins handled the situation, and found they did something different for the ‘add_filter’ call for the ‘plugin_meta_link’: the second parameter was an array where the first entry was the name (string) of the class under which the function was defined and the second entry was the name of the function. Still don’t understand how it works and how it was so different than other calls where I used ‘&$this’ for the first entry.
Oops. Make that “>=0” statements.
Thank you for the post, most of all about the plugin action and meta links. Really usable!
Great post, Slobodan! very usable. May I use this for my commercial theme?
Thanks!
You should 😉 That’s the whole point, to help make themes out there better!
Hey guys quick question,
(I couldn’t find the answer anywhere so I’m asking you directly), I downloaded one of your awesome themes, but I’m not sure how to install it or use it for my blog. Is this only for premium packages or can I use these customized themes on a free wordpress blog?
Thanks!
Jobi
Hey Joe,
The themes on our site are all for Self-Hosted WordPress powered sites (WordPress.org) if you are on WordPress.org you won’t be able to use them.
Thanks for the tips.
Just thought I’d let you know that the “Plugin Action and Meta Links” example uses admin_url( ‘/wp-admin/’ … ) which doubles up on the wp-admin.
Nice catch, I’m updating the snippet.
I’d also like to thank you. This is the first tutorial on contextual help that I’ve been able to follow and understand, and that gave me a working solution to build on.
I’m really glad to hear that, thanks!
How do you make the notice reappear after an update or download of the plugin. The code that you have only displays it one time.
Thanks
You just have to hook into the correct place:
http://codex.wordpress.org/Function_Reference/register_activation_hook
I’m not sure about the update though. You might have to have a function that runs on init and checks for a value in the database (added by your plugin) that holds the plugin version number and changes every time you update.