How to Create Custom Meta Boxes In WordPress

A custom meta (or write) box is incredibly simple in theory. It allows you to add a custom piece of data to a post or page in WordPress.

Conveniently, WordPress provides a function for adding meta boxes to a given admin screen: add_meta_box.

The codex entry is well done for this function, but here’s a brief overview. Its prototype:

 

<?php add_meta_box( $id, $title, $callback, $page, $context, $priority, $callback_args ); ?>

$id is the html ID attribute of the box. This is useful if you’re loading custom CSS or Javascript on the editing page to handle the options. Otherwise, it doesn’t really matter all that much.

$title is displayed at the top of the meta box.

$callback is the function that actually renders the meta box. We’ll outline this in step 2.

$page is where you want the meta box to be displayed. This should be a string with ‘post’ or ‘page’ or ‘some_custom_post_type’.

$context is where you want the meta box displayed. ‘normal’ puts it below the post editor. ‘side’ moves the meta box to editing screen’s right sidebar (by the categories and tags, etc). ‘advanced’ also put the box in the same column as the post editor, but further down.

$priority tells wordpress where to place the meta box in the context. ‘high’, ‘default’ or ‘low’ puts the box closer to the top, in its normal position, or towards the bottom respectively. Since all meta boxes are drag-able, $priority is not a huge deal.

Finally $callback_args lets you pass data to your $callback function in the form of an array. We’re not going to use this here, but it could be useful for passing some data to the meta box. Say, if your plugin had several options that influenced what was displayed in the meta box. You could pass those options values through the $callback_args array.

So, our add_meta_box call will look like this:

<?php add_meta_box( 'my-meta-box-id', 'My First Meta Box', 'cd_meta_box_cb', 'post', 'normal', 'high' ); ?>

We can’t just pop this into our plugin file alone. Doing so will result in the white screen of death, and PHP fatal error: call to undefined function. Why? Because we called add_meta_box function before WordPress was loaded. So we need to use a WordPress hook, which is part of the plugin api. Basically, functions get hooked into a given WordPress action or filter hook, then those functions are fired when that hook loads. By wrapping our add_meta_box call in a function, then hooking that function into the add_meta_boxes action hook, we avoid the fatal error.

Our code to add the meta box to our post screen would look like this:

<?php add_action( 'add_meta_boxes', 'cd_meta_box_add' ); function cd_meta_box_add() { add_meta_box( 'my-meta-box-id', 'My First Meta Box', 'cd_meta_box_cb', 'post', 'normal', 'high' ); } ?>

The above code is enough to add the meta box, but now we have to render the thing and actually add fields. This is just an HTML form code mixed in with a bit of PHP to display the saved data. We don’t need to include the form tags as WordPress does that for us.

Remember the string we passed as the $callback in add_meta_box? We’re now going to create a function with the same name. This function will take care of all the display inside the meta box.

<?php function cd_meta_box_cb() { echo 'What you put here, show\'s up in the meta box'; } ?>


We’re going to add several fields to our meta box: a text input, a drop down menu, and a check-box. Let’s start with the text input.

<?php function cd_meta_box_cb() { ?>
    <label for="my_meta_box_text">Text Label</label>
    <input type="text" name="my_meta_box_text" id="my_meta_box_text" />
    <?php } ?>

But what about actually displaying the data? Well, as you’ll see in step 3, we’ll store this data in the wp_postmeta table using the update_post_meta function. That function has two sister functions called get_post_meta and get_post_custom, which grab data from wp_postmeta. get_post_meta only grabs data from one key, while get_post_custom grabs all of it. Because we’re only really using one field at this point, let’s use get_post_meta.

Also note that the add_meta_box function passes one variable to our callback: $post, which is a post object.

<?php function cd_meta_box_cb( $post ) { $values = get_post_custom( $post->ID );
$text = isset( $values['my_meta_box_text'] ) ? esc_attr( $values['my_meta_box_text'][0] ) : ”;
$selected = isset( $values['my_meta_box_select'] ) ? esc_attr( $values['my_meta_box_select'][0] ) : ”;
$check = isset( $values['my_meta_box_check'] ) ? esc_attr( $values['my_meta_box_check'][0] ) : ”;
    ?>

    <label for="my_meta_box_text">Text Label</label>
    <input type="text" name="my_meta_box_text" id="my_meta_box_text" value="<?php echo $text; ?>" />
 
    <?php } ?>
<?php function cd_meta_box_cb( $post ) { $values = get_post_custom( $post->ID );
$text = isset( $values['my_meta_box_text'] ) ? esc_attr( $values['my_meta_box_text'][0] ) : ”;
$selected = isset( $values['my_meta_box_select'] ) ? esc_attr( $values['my_meta_box_select'][0] ) : ”;
$check = isset( $values['my_meta_box_check'] ) ? esc_attr( $values['my_meta_box_check'][0] ) : ”;
    ?>
    
        <label for="my_meta_box_text">Text Label</label>
        <input type="text" name="my_meta_box_text" id="my_meta_box_text" value="<?php echo $text; ?>" />
    
        <label for="my_meta_box_select">Color</label>
        <select name="my_meta_box_select" id="my_meta_box_select">
<option value="red" <?php selected( $selected, 'red' ); ?>>Red</option>
<option value="blue" <?php selected( $selected, 'blue' ); ?>>Blue</option>
        </select>
    
    <?php } ?>

With the addition of a second field, we changed or get_post_meta call to get_post_custom, which returns an associative array of all the post’s custom keys and values. We then just access our fields via their names. The ternary statements keep our code from throwing PHP warnings (undefined indices and such). We’ll cover the esc_attr function in step three.

In the drop down, we’re going to use one of WordPress’s most handy functions: selected. This compares the first value, the data we saved, with the second, the <option>‘s value attribute. If they’re the same, the function will echo selected="selected", which makes that value display on the drop down. Pretty sweet, and it saves us from writing a bunch of if or ternary statements. You can also use the selected() function with radio buttons.

<?php function cd_meta_box_cb() { // $post is already set, and contains an object: the WordPress post global $post; $values = get_post_custom( $post->ID );
    $text = isset( $values['my_meta_box_text'] ) ? $values['my_meta_box_text'] : '';
    $selected = isset( $values['my_meta_box_select'] ) ? esc_attr( $values['my_meta_box_select'] ) : '';
    $check = isset( $values['my_meta_box_check'] ) ? esc_attr( $values['my_meta_box_check'] ) : '';
     
    // We'll use this nonce field later on when saving.
    wp_nonce_field( 'my_meta_box_nonce', 'meta_box_nonce' );
    ?>
        <label for="my_meta_box_text">Text Label</label>
        <input type="text" name="my_meta_box_text" id="my_meta_box_text" value="<?php echo $text; ?>" />

        <label for="my_meta_box_select">Color</label>
        <select name="my_meta_box_select" id="my_meta_box_select">
<option value="red" <?php selected( $selected, 'red' ); ?>>Red</option>
<option value="blue" <?php selected( $selected, 'blue' ); ?>>Blue</option>
        </select>
    
        <input type="checkbox" id="my_meta_box_check" name="my_meta_box_check" <?php checked( $check, 'on' ); ?> />
        <label for="my_meta_box_check">Do not check this</label>
    
    <?php } ?>

Again WordPress provides the handy function checked(). It works just like selected() comparing the first value (our saved data) to the second and echoing out checked="checked" if they’re the same.

wp_nonce_field adds two hidden fields to our meta box. One of them is a nonce. These are random strings of numbers that are valid on per user per blog basis for 24 hours. Nonces are a way of verifying intention, and they make sure that WordPress doesn’t do anything unless the request came from a very specific place. In other words, we don’t want to accidentally update our data by somehow running our save function (see step 3) in another location other than the save_post hook, so we check to make sure the nonce is valid before doing anything.

The number one rule when putting anything into your database or on your site is don’t trust the user. Even if that user is you.

To save our data, we’re going to rely on another WordPress hook: save_post. This works just like our action hook above:

<?php add_action( 'save_post', 'cd_meta_box_save' ); ?>

The cd_meta_box_save function will receive one argument, the post id, and take care of cleaning and saving all of our data. The save_post hook fires after the update or save draft button has been hit. So we have access to all the $_POST data, which includes our meta box fields, within our saving function. Before we can do anything, however, we have to do three things: check if the post is auto saving, verify the nonce value we created earlier, and check to make sure the current user can actually edit the post.

<?php add_action( 'save_post', 'cd_meta_box_save' ); function cd_meta_box_save( $post_id ) { // Bail if we're doing an auto save if( defined( 'DOING_AUTOSAVE' ) &amp;&amp; DOING_AUTOSAVE ) return; // if our nonce isn't there, or we can't verify it, bail if( !isset( $_POST['meta_box_nonce'] ) || !wp_verify_nonce( $_POST['meta_box_nonce'], 'my_meta_box_nonce' ) ) return; // if our current user can't edit this post, bail if( !current_user_can( 'edit_post' ) ) return; } ?>

 

Now the fun stuff: actually saving our data. The number one rule when putting anything into your database or on your site is don’t trust the user. Even if that user is you. To that end, before we save any data, we want to make sure there’s nothing malicious in there. Fortunately WordPress provides a bunch of functions for data validation.

You already saw esc_attr() above (step 2). This one encodes ‘ ” and < > into HTML entities. Why use this? So users couldn’t type a <script> into your meta box. If you’d like to allow certain HTML tags in, but strip others, wp_kses can do that. It takes two arguments, the first of which is the string you’d like to check and the second is an associative array of allowed tags. WordPress provides many more data validation tools, just don’t be afraid to use them.

We’re going to use the update_post_meta function to date care of saving our data. It takes three arguments: a post ID, the meta key, and the value.

<?php add_action( 'save_post', 'cd_meta_box_save' ); function cd_meta_box_save( $post_id ) { // Bail if we're doing an auto save if( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return; // if our nonce isn't there, or we can't verify it, bail if( !isset( $_POST['meta_box_nonce'] ) || !wp_verify_nonce( $_POST['meta_box_nonce'], 'my_meta_box_nonce' ) ) return; // if our current user can't edit this post, bail if( !current_user_can( 'edit_post' ) ) return; // now we can actually save the data $allowed = array( 'a' => array( // on allow a tags
            'href' => array() // and those anchors can only have href attribute
        )
    );
     
    // Make sure your data is set before trying to save it
    if( isset( $_POST['my_meta_box_text'] ) )
        update_post_meta( $post_id, 'my_meta_box_text', wp_kses( $_POST['my_meta_box_text'], $allowed ) );
         
    if( isset( $_POST['my_meta_box_select'] ) )
        update_post_meta( $post_id, 'my_meta_box_select', esc_attr( $_POST['my_meta_box_select'] ) );
         
    // This is purely my personal preference for saving check-boxes
    $chk = isset( $_POST['my_meta_box_check'] ) && $_POST['my_meta_box_select'] ? 'on' : 'off';
    update_post_meta( $post_id, 'my_meta_box_check', $chk );
}
?>

That’s it! You should have a fully working meta box. Other examples you may find around the web loop through a bunch of fields without really cleaning the data. This is the wrong approach. Always use the built in data validation functions; different fields/values may require different data validation.