On Github ryelle / UX-For-WP-Developers
How easy something is to use and learn
Can tasks be completed without intense frustration?
UX goes a level above usability — it's about how enjoyable a product is. It's about creating a pleasurable experience for our users. It's about making our users' lives easier. Usability should just be the baseline, not the standard. We don't just want our users to not be frustrated, we want them to be positive, even delighted.
So, how many of you have installed a theme or a plugin, and then had no idea what to do next to get it working?
Yeah, that probably wasn't a great experience for you.
When you're adding features to wp-admin, there are two things you want to make sure you do.
Provide a familiar environment for your users to learn your new feature.
Don't make your users have to hunt around for things. Make them feel secure in knowing where things are.
New features, such as plugins and themes, should always be seamlessly integrated into the WordPress admin interface. It should never look like you're leaving WordPress — the fact that you're an extension of WordPress should be totally invisible. Your theme or plugin should feel like it belongs.
When at all possible, you should be using the Settings API to create new admin pages for your plugins or themes.
Otherwise, if WordPress decides to go and, oh, totally change the admin styles... Your new pages look terrible.
Adding a top-level menu should only be considered if you really need multiple, related screens to make WordPress do something it was not originally designed to accomplish, or are adding a totally new content type. Examples of new top-level menus might include conference management — tickets, speakers, sponsors, etc.
Group your settings page in with this menu.
If you don't need a top-level menu, decide which menu to add your plugin or theme options to. Most plugins add sub-level menu items within existing WordPress menus. For example, a backup plugin adds a page to the Tools top-level menu, or a gallery plugin could be added to the media top-level menu.
Pages should be labeled accurately so users will know what they will find on each page without having to guess. Keep labels short and concise.
Add your settings to existing settings pages, if it makes sense, otherwise add it as a sub-menu to the settings top-level menu.
If you are adding a top-level menu, then it's highly recommended that you add a custom icon which stylistically blends in with the rest of the admin icons.
Your icon should, by default, be black and white, with color on hover.
add_action( 'admin_menu', 'wcbos_settings_page' ); function wcbos_settings_page() { add_options_page( 'WordCamp', // Page title 'WCBos Example Settings', // Menu title 'manage_options', // Capability 'wcbos-2013', // Slug 'wcbos_settings_page_render' // Display callback ); }
There are a few other functions for this: add_dashboard_page(), add_posts_page(), add_media_page(), add_links_page(), add_pages_page(), add_comments_page(), add_theme_page(), add_plugins_page(), add_users_page(), add_management_page() [tools], add_options_page(). More generically, add_submenu_page() for any page, will also let you add to a CPT.
add_menu_page to add a page as a top-level item.
add_filter( 'admin_init', 'wcbos_register_fields' ); function wcbos_register_fields() { register_setting( 'wcbos_options', // Option group (used to display fields) 'wcbos_option', // Option name 'wcbos_validate' // Validation callback ); // ... continued later ... }
Later slides will continue this function (wcbos_register_fields), it's here that we set up all our fields.
function wcbos_settings_page_render() { ?> <div class="wrap"> <h2>WordCamp Boston Options</h2> <form action="options.php" method="post"> <?php settings_fields( 'wcbos_options' ); ?> <?php do_settings_sections( 'wcbos_options' ); ?> <?php submit_button(); ?> </form> </div> <?php }
settings_fields() • do_settings_sections() • submit_button()
// ... inside wcbos_register_fields() add_settings_section( 'wcbos_first_section', // ID __( "Meetup API Settings" ), // Title 'wcbos_settings_first_section', // Display callback 'wcbos_options' // Page ); // ... more settings ...
function wcbos_settings_first_section() { _e( "This is a description of the first section." ); }
You can also pass false to add_settings_section for the callback, to only display the section title.
// ... inside wcbos_register_fields() add_settings_field( 'wcbos_text_one', // ID __( "First Name" ), // Title 'wcbos_settings_text_field', // Display callback 'wcbos_options', // Page 'wcbos_first_section', // Section array( 'label_for' => 'wcbos_text_one' ) // Args ); // ... more settings ...
function wcbos_settings_text_field( $args ) { if ( ! isset( $args['label_for'] ) ) return; $id = $args['label_for']; $values = get_option( 'wcbos_option' ); printf( '<input type="text" name="%1$s" id="%1$s" value="%2$s" />', "wcbos_option[$id]", esc_attr( $values[$id] ) ); }
We're using label_for as an ID, so we can create a "generic" text input function.
// ... inside wcbos_register_fields() add_settings_field( 'wcbos_dropdown', // ID __( "Options" ), // Title 'wcbos_settings_dropdown', // Display callback 'wcbos_options', // Page 'wcbos_first_section', // Section array( 'label_for' => 'wcbos_dropdown' ) // Args ); // ... more settings ... } function wcbos_example_options() { $options = array( 'a' => __( "Option A" ), 'b' => __( "Option B" ), 'c' => __( "Option C" ), ); return apply_filters( 'wcbos_example_options', $options ); }
function wcbos_settings_dropdown( $args ) { if ( ! isset( $args['label_for'] ) ) return; $id = $args['label_for']; $values = get_option( 'wcbos_option' ); $options = wcbos_example_options(); // Filterable list of choices echo '<select name="wcbos_option['.$id.']">'; foreach ( $options as $key => $view ) { printf( '<option value="%s" %s>%s</option>', $key, selected( $key, $values[$id], false ), $view ); } echo '</select>'; }
function wcbos_register_fields() { register_setting( 'wcbos_options', // Option group (used to display fields) 'wcbos_option', // Option name 'wcbos_validate' // Validation callback ); add_settings_section( 'wcbos_first_section', // ID __( "Meetup API Settings" ), // Title 'wcbos_settings_first_section', // Display callback 'wcbos_options' // Page ); add_settings_field( 'wcbos_text_one', // ID __( "First Name" ), // Title 'wcbos_settings_text_field', // Display callback 'wcbos_options', // Page 'wcbos_first_section', // Section array( 'label_for' => 'wcbos_text_one' ) // Args ); add_settings_field( 'wcbos_dropdown', // ID __( "Options" ), // Title 'wcbos_settings_dropdown', // Display callback 'wcbos_options', // Page 'wcbos_first_section', // Section array( 'label_for' => 'wcbos_dropdown' ) // Args ); }
We've created a text field and a dropdown, now we should validate/sanitize before saving.
In register_setting we defined a validation callback wcbos_validate.
function wcbos_validate( $input ) { $output = array(); // If set and valid if ( isset( $input['wcbos_text_one'] ) ){ $output['wcbos_text_one'] = sanitize_text_field( $input['wcbos_text_one'] ); } $options = wcbos_example_options(); if ( isset( $input['wcbos_dropdown'] ) ){ if ( in_array( $input['wcbos_dropdown'], array_keys( $options ) ) ) { $output['wcbos_dropdown'] = $input['wcbos_dropdown']; } } return $output; }
Most items you could put in a theme options page can go somewhere else.
Don’t take items that should be in the Appearance menu (like custom headers, background, etc.) and put them in your screen options page. They should stay as options in the Appearance menu. Also add them to the customizer!
add_theme_support( 'custom-background', $optional_defaults ); add_theme_support( 'custom-header', $optional_defaults );
Have something like a logo uploader? Give it its own page, "Custom Logo", instead of "theme options". It's more intuitive, and if a user sees that, they know what it means.
Have something like a theme slider? That could also be given a uniquely titled page.
add_action( 'admin_menu', 'wcbos_theme_page' ); function wcbos_theme_page() { add_theme_page( 'Custom Logo', // Page title 'Custom Logo', // Menu title 'manage_options', // Capability 'custom-logo', // Slug 'wcbos_theme_page_render' // Display callback ); }
Does this look familiar? If you've been paying attention, it should, as it's the same as the beginning of the settings API section.
Let's talk a little bit about making good UI decisions. This is where we're going to go from your plugin or theme being just usable, to it being a great experience.
We stole this from Helen. Thanks Helen.
Make it easier for the user to find what they need
In this case, they had a bunch of different taxonomies that could be applied to their custom post type. Instead of having a huge jumble of checkbox lists, they consolidated it into one metabox with smarter selection using a library called Chosen. Then for age, instead of checkboxes, they replaced it with a slider.
Pick smarter locations:
Remove what you don't need
Replace what you do (strings, etc.)
What are the options or features that 80% of your users need?
Hide the rest in screen options (and document where they are)
If users do something, they're expecting to receive feedback — a hover state on a button, a success message, something — always provide your users with that kind of feedback.
This is also a great place to sneak in some personality — an icon that changes from to a fun spinner while you save, a happy face when you complete a task, etc.
"Experiencing an app without good design feedback is like getting your high-five turned down."
~ @strevatIf you're telling your users something, generally it's something you want them to see. Don't let it fade into the background — make it another color, give it some special treatment, make it big... Just get their attention.
add_action( 'init', 'wcbos_create_post_type' ); function wcbos_create_post_type() { register_post_type( 'wcbos-people', array( 'labels' => array( 'name' => 'People', 'singular_name' => 'Person', 'add_new' => 'Add Person', 'add_new_item' => 'Add New Person', 'edit_item' => 'Edit Person', 'new_item' => 'New Person', 'search_items' => 'Search People', 'not_found' => 'No people found', 'not_found_in_trash' => 'No people found in trash' ), 'public' => true, 'supports' => array( 'title', 'editor', 'thumbnail' ), 'rewrite' => array( 'slug' => 'person' ), 'has_archive' => 'people', ) ); // ... continued
Title, editor, featured image
Generally post types are singular names, this was a mistake I realized last night.
// ... inside wcbos_create_post_type() register_taxonomy( 'wcbos-team', 'wcbos-people', array( 'labels' => array( 'name' => 'Teams', 'singular_name' => 'Team', 'all_items' => 'All Teams', 'edit_item' => 'Edit Team', 'view_item' => 'View Team', 'update_item' => 'Update Team', 'add_new_item' => 'Add New Team', 'new_item_name' => 'New Team Name', 'search_items' => 'Search Teams', 'popular_items' => 'Popular Teams', 'parent_item' => 'Parent Team', 'parent_item_colon' => 'Parent Team:', ), 'hierarchical' => true, 'show_admin_column' => true, 'rewrite' => array( 'slug' => 'team' ), ) ); }
// inside the register_post_type args in wcbos_create_post_type() 'has_archive' => 'people', 'register_meta_box_cb'=> 'wcbos_add_metaboxes', ) ); } function wcbos_add_metaboxes() { // New metabox for person information: twitter, website, etc add_meta_box( 'wcbos-people-info', // ID __( "Social Media" ), // Title 'wcbos_people_info_box', // Display callback 'wcbos-people', // Post type 'normal', // Context, 'normal', 'advanced', or 'side' 'default' // Priority, 'high', 'core', 'default' or 'low' ); // ... continued later ... }
function wcbos_people_info_box() { $twitter = get_post_meta( $post->ID, 'twitter', true ); ?> <p> <label for="info_twitter">Twitter:</label> <input type="text" name="info_twitter" value="<?php echo esc_attr( $twitter ); ?>" id="info_twitter" /> </p> <?php }
add_action( 'save_post', 'wcbos_save_post_meta' ); function wcbos_save_person_meta( $post_id ) { if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return; if ( ! current_user_can( 'edit_post', $post_id ) ) return; if ( ! isset( $_POST['person-info'] ) || ! wp_verify_nonce( $_POST['person-info'], 'save' ) ) return; if ( isset( $_POST['info_twitter'] ) ){ update_post_meta( $post_id, // Post ID 'twitter', // Meta key sanitize_text_field( $_POST['info_twitter'] ) // Value ); } }
// inside wcbos_add_metaboxes() remove_meta_box( 'postimagediv', // ID 'wcbos-people', // Post type 'side' // Context ); add_meta_box( 'postimagediv', __( 'Photo' ), 'post_thumbnail_meta_box', 'wcbos-people', 'side' ); }
add_action( 'admin_head', 'wcbos_custom_icons' ); function wcbos_custom_icons() { ?> <style type="text/css" media="screen"> #adminmenu #menu-posts-wcbos-people .wp-menu-image { /* Sprite for non-MP6 */ background-image: url(<?php echo plugins_url('admin-users.png',__FILE__); ?>); background-position: 5px 4px; } #adminmenu #menu-posts-wcbos-people.wp-has-current-submenu .wp-menu-image, #adminmenu #menu-posts-wcbos-people a:hover .wp-menu-image { background-position: 5px -23px; } </style> <?php }
add_action( 'admin_head', 'wcbos_custom_icons' ); function wcbos_custom_icons() { ?> <style type="text/css" media="screen"> .mp6 #adminmenu #menu-posts-wcbos-people .wp-menu-image:before { content: '\f307'; } </style> <?php }
<?php add_filter( 'post_updated_messages', 'wcbos_people_updated_messages' ) ); function wcbos_people_updated_messages( $messages = array() ) { global $post, $post_ID; $messages['wcbos-people'] = array( 0 => '', // Unused. Messages start at index 1. 1 => sprintf( __( 'Person updated. <a href="%s">View person</a>.', 'textdomain' ), esc_url( get_permalink( $post_ID ) ) ), 2 => __( 'Custom field updated.', 'textdomain' ), 3 => __( 'Custom field deleted.', 'textdomain' ), 4 => __( 'Person updated.', 'textdomain' ), /* translators: %s: date and time of the revision */ 5 => isset( $_GET['revision'] ) ? sprintf( __( 'Person restored to revision from %s', 'textdomain' ), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false, 6 => sprintf( __( 'Person published. <a href="%s">View slide</a>', 'textdomain' ), esc_url( get_permalink( $post_ID ) ) ), 7 => __( 'Person saved.', 'textdomain' ), 8 => sprintf( __( 'Person submitted. <a target="_blank" href="%s">Preview slide</a>', 'textdomain' ), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post_ID ) ) ) ), 9 => sprintf( __( 'Person scheduled for: <strong>%1$s</strong>. <a target="_blank" href="%2$s">Preview person</a>', 'textdomain' ), // translators: Publish box date format, see http://php.net/date date_i18n( __( 'M j, Y @ G:i', 'textdomain' ), strtotime( $post->post_date ) ), esc_url( get_permalink( $post_ID ) ) ), 10 => sprintf( __( 'Person draft updated. <a target="_blank" href="%s">Preview person</a>', 'textdomain' ), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post_ID ) ) ) ), ); return $messages; }
<?php add_filter( 'enter_title_here', 'wcbos_people_enter_title', 10, 2 ); function wcbos_people_enter_title( $text, $post ) { if ( $post->post_type == 'wcbos-people' ) $text = __( "Enter name here" ); return $text; }
A formalized method for testing your product with your user group
Qualitative, usually pretty informal
You can do it cheap, it's easy, and it's fast.
All sites/products have problems. Most serious problems get identified pretty quickly — Great way to identify the biggest usability issues in your product. Usually also identifies a lot of quick wins to fix
It makes you a better creator
Steve Krug says 3, Jakob Nielsen says 5, but even 1-2 are better than 0
Tasks are things like: Install plugin. Configure plugin to do X thing. Check to see if it is working. etc.
Should be the most important features of your product
Scenario: you're a photo blogger and you want to install a plugin which enhances WordPress' internal galleries. You've heard x plugin is great. Install x plugin to your WordPress site.
Lots of services to do this: camtasia, silverback, etc.
If your users asks you what to do or are confused, ask them where they think they'd be able to do x
For wp core and wpcom, we use usertesting.com