UX For Theme and Plugin Developers

Mel Choyce [@melchoyce] • Kelly Dwan [@ryelle]

Mel Choyce

Design Engineer
at Automattic

Kelly Dwan

Web Engineer
at 10up

Intro to User Experience

What is Usability?

What is UX?

UX within wp-admin

  1. Provide a familiar environment

  2. Don't make people hunt

Be one with WordPress

Use the Settings API

Put your options in logical locations

When to make top-level menus

When to make sub-menus

Create custom icons

:(

Settings API

  1. Create a new page
  2. Tell WordPress about your settings
  3. Display the (empty) page
  4. Create a section on the page
  5. Create individual fields
  6. Validate & save these fields

Create a new page


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
	);
}						

add_options_page()

Tell WordPress about your settings


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 ...
}						

register_setting()

Display the (empty) page


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()

Create a section on the page (setup)


// ... 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 ...

add_settings_section()

Create a section on the page (display)


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.

Creating individual fields (setup)


// ... 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 ...

add_settings_field()

Creating individual fields (display)


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.

Creating a dropdown field (setup)


// ... 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 );
}						

Creating a dropdown field (display)


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>';
}						

selected()

All of register


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
	);
}						

Saving/validating these fields

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;
}						

sanitize_text_field()

You probably don't need
a "themes options" page.

Use the core Custom Header & Custom Background functions


add_theme_support( 'custom-background', $optional_defaults );
add_theme_support( 'custom-header', $optional_defaults );
						

Custom HeadersCustom Backgrounds

But I have special settings!

Creating a settings page under Appearance


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
	);
}						

add_theme_page()

Make better UI decisions

Make information more digestible

We stole this from Helen. Thanks Helen.

Place elements in logical locations

Tailor your screens

Don't overwhelm users with options

Respond to user's actions

"Experiencing an app without good design feedback is like getting your high-five turned down."

~ @strevat

Clearly mark alerts and messages

Write good error messages

Custom Content

  1. Create a post type with the correct defaults
  2. Create a custom taxonomy
  3. Add any extra fields to the post type
  4. Update the post type icon
  5. Update any strings with relevant text

Create a post type with the correct defaults


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

register_post_type()

Create a Custom Taxonomy


	// ... 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' ),
	) );
}						

register_taxonomy()

Extra Fields on a Post Type (setup)


	// 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 ...
}						

add_meta_box()

Extra Fields on a Post Type (display)


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
}						

get_post_meta()

Extra Fields on a Post Type (saving)


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
		);
	}

}
						

save_postupdate_post_meta()

Renaming the Featured Image


// 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' 
	);
}						

remove_meta_box()

Update the Post Type Icon (non-MP6)


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 }

						

Update the Post Type Icon (MP6)


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 }
						

More examples, using custom font & SVG

Update Strings With Relevant Text


<?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. View person.', '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. View slide', 'textdomain' ), esc_url( get_permalink( $post_ID ) ) ),
		7  => __( 'Person saved.', 'textdomain' ),
		8  => sprintf( __( 'Person submitted. Preview slide', 'textdomain' ), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post_ID ) ) ) ),
		9  => sprintf( __( 'Person scheduled for: %1$s. Preview person', '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. Preview person', 'textdomain' ), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post_ID ) ) ) ),
	);
	
	return $messages;
}
						

post_updated_messages

Update Strings With Relevant Text


<?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;
}
						

enter_title_here

User Testing

What

Why

  1. Cheap
  2. Easy
  3. Fast

How

Recruit to fill 3-5 tests

Write your tasks

Turn tasks into scenarios

Test run your test internally

Record screen

Don't explain, just watch

You can test remotely, too

Rocket Surgery Made Easy

Let's do it!

THE END