How to Add a Metabox to a Custom Post Type

Most custom post types in WordPress will need (or could benefit from) a unique set metaboxes for entering information.

For example, a “photography” post type might need fields for “location of photo”, “type of camera”, etc. And an “event” post type would probably need a “location” and an “event date”.

Metaboxes aren’t the easiest to set up- so I’ve written up this tutorial which shows how to add a one line field for “location” to an “event” post type.

Hopefully, you’ll be able to use this guide to add any sort of metaboxes you need.

Set Up the Post Type

If you are unfamiliar with how to set up custom post types, check out Justin Tadlock’s excellent tutorial. For this example, I am going to use a post type called “Event”, which goes in my functions.php file:

/**
* Registers the event post type.
*/
function wpt_event_post_type() {
$labels = array(
'name' => __( 'Events' ),
'singular_name' => __( 'Event' ),
'add_new' => __( 'Add New Event' ),
'add_new_item' => __( 'Add New Event' ),
'edit_item' => __( 'Edit Event' ),
'new_item' => __( 'Add New Event' ),
'view_item' => __( 'View Event' ),
'search_items' => __( 'Search Event' ),
'not_found' => __( 'No events found' ),
'not_found_in_trash' => __( 'No events found in trash' )
);
$supports = array(
'title',
'editor',
'thumbnail',
'comments',
'revisions',
);
$args = array(
'labels' => $labels,
'supports' => $supports,
'public' => true,
'capability_type' => 'post',
'rewrite' => array( 'slug' => 'events' ),
'has_archive' => true,
'menu_position' => 30,
'menu_icon' => 'dashicons-calendar-alt',
'register_meta_box_cb' => 'wpt_add_event_metaboxes',
);
register_post_type( 'events', $args );
}
add_action( 'init', 'wpt_event_post_type' );

You may have your own custom post type set up completely different, but that’s fine. The important line of code for the metaboxes is ‘register_meta_box_cb’ => ‘add_events_metaboxes’- which calls the function to build the metaboxes.

You can rename the function to whatever you like, for instance ‘register_meta_box_cb’ => ‘add_photography_metaboxes’ might be better for a photography post type.

If the post type is being registered through a plugin or is one of the native post types, you can also use:

add_action( 'add_meta_boxes', 'add_events_metaboxes' );

Add Meta Box

The following code adds a metabox to the right side of the screen under the “Publish” box:

/**
* Adds a metabox to the right side of the screen under the “Publish” box
*/
function wpt_add_event_metaboxes() {
add_meta_box(
'wpt_events_location',
'Event Location',
'wpt_events_location',
'events',
'side',
'default'
);
}

You can read the full parameters for add_meta_box in the codex. I also listed them here:

add_meta_box( $id, $title, $callback, $page, $context, $priority, $callback_args );

For the example above:

  • $id is “wpt_events_location”- or the html id that will be applied to this metabox.
  • $title is “Event Location”. This appears at the top of the new metabox when displayed.
  • $callback is the function “wpt_events_location” which will load the html into the metabox.
  • $page is “events”, the name of our custom post type.
  • $context is “side”. If you wanted it to load below the content area, you could put “normal”.
  • $priority controls where the metabox will display in relation to the other metaboxes. You can put “high”, “low” or “default”.

If you wanted to have two sets of metaboxes, perhaps one on the side and one below the content area, you could do something like this (Note: Don’t use this if you’re following the tutorial step by step, this is just an example of how it would be done):

/**
* If you wanted to have two sets of metaboxes.
*/
function add_events_metaboxes_v2() {
add_meta_box(
'wpt_events_date',
'Event Date',
'wpt_events_date',
'events',
'side',
'default'
);
add_meta_box(
'wpt_events_location',
'Event Location',
'wpt_events_location',
'events',
'normal',
'high'
);
}

You’d then have to make sure the two function wpt_events_date and wpt_events_location were defined to call the html code to go inside the metaboxes.

Generating the HTML for the Metabox

Continuing with the first example above, we’ll now have to generate the code that goes inside our “Event Location” metabox. To keep this as simple as possible, we’re just going to make one field:

/**
* Output the HTML for the metabox.
*/
function wpt_events_location() {
global $post;
// Nonce field to validate form request came from current site
wp_nonce_field( basename( __FILE__ ), 'event_fields' );
// Get the location data if it's already been entered
$location = get_post_meta( $post->ID, 'location', true );
// Output the field
echo '<input type="text" name="location" value="' . esc_textarea( $location ) . '" class="widefat">';
}

At this point you should have a metabox showing up in your post. If you check your “events” post type, it should load on the right side like in the screenshot I posted. This will generate any html you choose, so, you could put as many input fields in here as you like, or html descriptions.

In order to class the inputs and descriptions correctly, check out the source code for other write panels in WordPress. See how they do textareas and select boxes. You can even add icons and generated text in these spots.

Saving

If you had tried to save your metabox data before this point, it just would have disappeared on the refresh because it wasn’t being saved. Here’s the code that updates the metabox when you click “Update”:

/**
* Save the metabox data
*/
function wpt_save_events_meta( $post_id, $post ) {
// Return if the user doesn't have edit permissions.
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return $post_id;
}
// Verify this came from the our screen and with proper authorization,
// because save_post can be triggered at other times.
if ( ! isset( $_POST['location'] ) || ! wp_verify_nonce( $_POST['event_fields'], basename(__FILE__) ) ) {
return $post_id;
}
// Now that we're authenticated, time to save the data.
// This sanitizes the data from the field and saves it into an array $events_meta.
$events_meta['location'] = esc_textarea( $_POST['location'] );
// Cycle through the $events_meta array.
// Note, in this example we just have one item, but this is helpful if you have multiple.
foreach ( $events_meta as $key => $value ) :
// Don't store custom data twice
if ( 'revision' === $post->post_type ) {
return;
}
if ( get_post_meta( $post_id, $key, false ) ) {
// If the custom field already has a value, update it.
update_post_meta( $post_id, $key, $value );
} else {
// If the custom field doesn't have a value, add it.
add_post_meta( $post_id, $key, $value);
}
if ( ! $value ) {
// Delete the meta key if there's no value
delete_post_meta( $post_id, $key );
}
endforeach;
}
add_action( 'save_post', 'wpt_save_events_meta', 1, 2 );

This code checks to make sure the user has privileges to update the post, then saves the data that’s in the event_location field.

Other Resources

If you need to add a lot of custom meta fields (especially more complex ones like date pickers, file uploaders, etc) you may want to consider using a library like CMB2 or Advanced Custom Fields.

I also created a boilerplate plugin for a team post type with metaboxes if you’d like to view the code for that.

All the code for this post is here. Please share and enjoy.

About Devin

I am a developer based in Austin, Texas. I run a little theme shop called DevPress and work for a startup called Nano. Find me on twitter @devinsays.

124 Responses

  1. Jonathan Turner

    I have to second that comment. So many similar tutorials forget to indicate exactly where to place the code. Thanks again for putting this together.

  2. Eric Curtis

    Nice tutorial. I will be using it in a few hours. BTW the link to the other article at the bottom appears to be broken. Cheers.

  3. I added a metabox for a custum post type. I added jQuery UI datepicker to an input field inside this meta box.
    It works but has a bit of a flaw:
    I had to load jQuery UI core from google apis (local WP version wouldn’t work) using enqueue script. Now however, it loads this for the entire WordPress Admin UI and this seems to interfere with the default jquery scripts on WordPress Admin Panel. (the boxes in the sidebar don’t slide open anymore, I have to reload before it works.)
    Now, for my question: how can I load the scripts and styles for my datepicker so they only load on my custom post type admin UI?
    Is there a function which does something like this?:
    if((is_admin)&&(is_mycustomposttype)){
    load scripts;
    }
    Any ideas how to solve this problem?

  4. @Devin Thank you for the reply.
    Where would you put this if ($post->post_type == ‘events’) in the above example? I can’t make it work.

    I am currently trying out a different path:
    add_action(‘admin_print_scripts-(page-hook)’, ‘init_enqueues’);
    But the problem with this one is that I don’t know what the page-hook for a custom post type is.

    1. @Peter, you put it in your enqueues function. Here is mine for example..

      // Sermon JavaScript
      add_action(‘admin_print_scripts’, ‘sermon_js’);
      function sermon_js() {
      global $post;
      if($post->post_type == ‘sermon’) {
      wp_enqueue_script(‘hh-jquery-ui’, ‘https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.11/jquery-ui.min.js’, array(‘jquery’));
      }
      }

  5. Jason Dugdale

    Good stuff! Using this code I’ve edited your Portfolio Theme to add a “Portfolio Site URL” – allowing me to change the featured image into a link to the website I built.
    It might even be worth adding this into your theme for the next release?
    Works a charm – Thanks!

  6. tripdragon

    Question for metaboxes. Has anyone tried to recreate the + Add New Category box? For creating a metabox that would just be full of list items say example some html links.

    So instead of leaving up to the client to try an use a wysiwyg metabox they would just click add new and paste a link. repeat

    Also instead of adding a set amount of metaboxes like 10 or more [ link 1, link 2… ]

  7. tripdragon

    I want the functionality of the box that creates the list true. But I need the form entries to be outside href links, not in site searches.

  8. Devin, I spent a bit of time trying to track down how to do this and, fortunately, ended up on your site. Thanks a lot for sharing the code in an expertly articulated manner.

  9. Zissou

    Thank you Devin for this tutorial, it really helped me out a lot.
    I do however have one question. If i want to have a certain meta box on multiple custom post types, do i have to code the same box over and over again but with a different reference everytime?
    Let’s say i want to have the “Location” -box on more than just the events post type, how do i repeat the function to work on other post types?

  10. Great tutorial thanks :) Most tutorials about add_meta_box hardly go over anything but you pretty much went over everything and your code actually works unlike other tutorials I’ve tried.

    Thanks!

  11. OMG THANK YOU SO MUCH FOR WRITING THIS TUTORIAL!! I was begining to think that creating a custom meta box was WAY harder than I anticipated because every tutorial I went through was crazy complex and dealing with a LOT of form checks and file handling.. All I needed was a simple concept and I could go from there.. SO THANK YOU!

    Thank being said.. I tried adding multiple meta boxes per your little ‘aside’ that you had in this tut. The problem I am having is that I have two meta boxes and can write in them, but only the last meta box saves the info.

    I scanned the code to see if i had a typo and didn’t see one. I also looked at your save code and if i am correct, it saves anything that has the events_meta function correct?

    I renamed your samples from EVENTS to SCHEDULES and from LOCATION to MONDAY and DATES to TUESDAY

    Here is my code:

    [EDITED]

  12. TED©

    Hi, i’m using this function and I’ve a problem: when I add “echo get_post_meta($post->ID, “_location”, true);” won’t work in single post. If I manually add the ID of the custom post, it works fine. If I don’t, nothing happens. Why? I put the “echo get_post_etc” inside and outside of the loop: same results.

  13. Michelle

    First, THANKS!! This is a great tutorial and finally helped me understand custom post types and how they interact with custom meta boxes. Whew!

    I had it all working until I added an additional field in my meta box. Now, everything works correctly *except* I can’t get the data from the meta boxes to display on my site. They save correctly in the post, repopulate etc.

    [Code removed]

    Any idea why this would be? The data did display correctly when I only had one field in the meta box…

    Thanks so much for your help and the great tutorial!

    Michelle

  14. Michelle'

    Thanks Devin! I’ve got it figured out – I combined what you taught me with WPAlchemy and have it all going strong. :)

  15. Piet

    Thanks for your excellent tutorial, Devin!

    Like a few other readers already commented, I looked at quite a few different tutorials before I found yours. Yours is the only one that really works! And it is clearly written and explained!

  16. Here is a question that is definitely “noob-esque”, but, if I have added multiple metaboxes (a textarea field below the text editor and a text input field on the side) do I then have to create a unique callback and save function for each one?

  17. As I am playing with the code, I found this line and am wondering what the numbers “1” and “2” are referring to:

    add_action('save_post', 'wpt_save_events_meta', 1, 2);

  18. Gian

    Hi, I am using this code for all my metaboxes now.
    Beside having textfields, are checkboxes supported?

    I am trying to have a checkbox to be checked in order to style some events differently based on the checkbox.

    It does show in the metabox, but doesn’t keep the checked status after I check and update the post.

    Any help?

  19. Charles

    This tutorial was great and really helped out a lot, def appreciate the effort put into this. One question:

    I have a dozen custom metabox inputs and they all save correctly and get dumped into custom fields. However, they don’t save into the fields in the custom metabox. So, if you go in to update/edit a field and leave the fields blank that don’t need editing, it deletes those custom fields as their values are null, removing them from the post meta entirely. I’ve tried playing around with the save function, but to no avail. The only way to keep them is to fill out every field every time you need to edit one of the post meta items.

    Thoughts?

      1. Charles

        No, this is the part that fails… It is storing the first two values in their respective fields, but the other ten are blank after save. This is a screenshot of the fields after filling them all out and hitting save:

        http://min.us/mvnrjC

        This is a screenshot of the custom field area, showing that all the values have been dumped into post meta values:

        http://min.us/mvjj0j

        I have no idea what’s not happening that the metabox fields come back blank. Oddly, the first time, they saved none. Then I got one to stick, then two, but no more since then.

  20. Hi. i couldn’t find this question answered above, so i’ll go ahead and ask it. How would i handle adding multiple sets of meta boxes. I can see how to add multiple fields…you’ve done it above. however, what about separate meta boxes. what i have to use all the function code twice, and use different names each time?

    thanks in advance

  21. Amazing tutorial. Thank you! Quick question:

    Is it possible for there to be a button that allows the user to add a field? So for example, if you wanted to give them the ability to add multiple links but didn’t want to constrain the amount, but rather just have them add a new field for each link. Does that make sense? I know how to display multiple values of a custom field in the theme, but if there was a way to add fields in the metabox that would make it easier for the user than adding a “link” custom field 5 times.

    Thanks!

  22. Also – Having a bit of issues with textareas. I’m looking to create one where the user enters the google maps iframe embed codes, and it tends to work the first time they update (in that it shows it) but it’s having a hard time saving. It strips out everything up to the closing /iframe tag. Any suggestions?

  23. Dave

    Great Tutorial.

    Really cleared things up for me, just one question…

    I’ve got a meta box set up with four fields, how do i get the title of the fields i.e. ‘dresscode’ to appear alongside what has been inputted. ie.

    dresscode: smart casual.

    Thanks

  24. Bob Jones

    Devin, here’s the code that is supposed to provide the output:
    ID, "_location", true); ?>

    OK, now where do we paste it? I tried events.php, archive-events.php, location.php, archive-location.php, and I get a 404 every time.

    I can output the contents of the Events (Add new Event) edit window with a standard loop in index.php. But what is the purpose of the code you provided? How do we output not just the contents of the edit window, but the Location and Dresscode fields as well? How do we output an Events archive?

    Thanks!

  25. Bob Jones

    I’d like to add a textarea field and size it with rows and columns, but I’m not sure how to do this.

    If, say, you were to make _location a textarea field and size it, how would you do so?

    Thanks.

      1. Bob Jones

        Global options? Well, now I’m just more confused than ever. I’ve read too many confusing tutorials, and absorbed just enough to patch together an operative custom post type. Yours is the closest to an all-in-one solution I’ve found, so I’d like to stick with it.

        The only way I’ve managed to output custom post type/metabox content is by adding ‘has_archive’ => true to register_post_type, then using a standard loop in a template named archive-my_template.php. Everything else — custom loops or page templates — is a bust. 404 errors all day long.

        And again, I’ve seen at least three different methods to echo metabox content. Using
        your method, how would I set the size of a textarea field?

        Thanks again.

  26. Hi Devin,

    I followed your tutorial and it works great. the custom post type and meta boxes are created and they save.

    However, when editing normal posts and pages i get the following error when I publish or update:

    Warning: Cannot modify header information – headers already sent by (output started at /Library/WebServer/Documents/langley/wp-content/themes/langley/functions.php:87) in /Library/WebServer/Documents/langley/wp-includes/pluggable.php on line 897

    The code on that line is:
    if ( !wp_verify_nonce( $_POST['eventmeta_noncename'], plugin_basename(__FILE__) )) {
    return $post->ID;
    }

    I know that that error message is normally down to whitespace in PHP files but I’ve tried to delete any and I’ve exported into textedit and imported again. Nothing seems to get rid of it!

    Any help is much, much appreciated!

    Keith

    1. Albert

      I’m having the same problem, including an Undefined index error:

      Notice: Undefined index: product_noncename in /Applications/MAMP/htdocs/test_site/wordpress/wp-content/themes/wookie/functions.php on line 328

      Line 328 points to:
      if ( !wp_verify_nonce( $_POST[‘product_noncename’], plugin_basename(__FILE__) )) {

      Any help on how to fix this, even though it’s just a notice.

  27. Lisa

    I’ve tried to modify this for a date field, but two things aren’t happening: (1) when I go back to edit the date information, it’s not showing up in the meta box, and (2) the date information also isn’t showing up in the HTML when I use this tag: ID, “_date”, true); ?>

    So, I’m wondering if it isn’t even saving the date information at all. Here’s my code:

    // Registers the new post type and taxonomy

    function wpt_event_posttype() {
    register_post_type( 'events',
    array(
    'labels' => array(
    'name' => __( 'Events' ),
    'singular_name' => __( 'Event' ),
    'add_new' => __( 'Add New Event' ),
    'add_new_item' => __( 'Add New Event' ),
    'edit_item' => __( 'Edit Event' ),
    'new_item' => __( 'Add New Event' ),
    'view_item' => __( 'View Event' ),
    'search_items' => __( 'Search Event' ),
    'not_found' => __( 'No events found' ),
    'not_found_in_trash' => __( 'No events found in trash' )
    ),
    'public' => true,
    'supports' => array( 'title', 'editor', 'thumbnail', 'comments', 'excerpt'),
    'capability_type' => 'post',
    'rewrite' => array("slug" => "events"), // Permalinks format
    'menu_position' => 5,
    'register_meta_box_cb' => 'add_events_metaboxes'
    ));

    flush_rewrite_rules( false );

    }
    add_action( 'init', 'wpt_event_posttype' );

    // Add the Events Meta Box

    function add_events_metaboxes() {
    add_meta_box('wpt_events_date', 'Event Date', 'wpt_events_date', 'events', 'side', 'default');
    }

    // The Event Date Metabox

    function wpt_events_date() {
    global $post;

    // Noncename needed to verify where the data originated
    echo '';

    // Get the date data if its already been entered
    $date = get_post_meta($post->ID, 'events_date', true);

    // Echo out the field
    echo '';

    // Save the Metabox Data

    function wpt_save_events_meta($post_id, $post) {

    // verify this came from the our screen and with proper authorization,
    // because save_post can be triggered at other times
    if ( !wp_verify_nonce( $_POST['eventmeta_noncename'], plugin_basename(__FILE__) )) {
    return $post->ID;
    }

    // Is the user allowed to edit the post or page?
    if ( !current_user_can( 'edit_post', $post->ID ))
    return $post->ID;

    // OK, we're authenticated: we need to find and save the data
    // We'll put it into an array to make it easier to loop though.

    $events_meta['events_date'] = $_POST['events_date'];

    // Add values of $events_meta as custom fields

    foreach ($events_meta as $key => $value) { // Cycle through the $events_meta array!
    if( $post->post_type == 'revision' ) return; // Don't store custom data twice
    $value = implode(',', (array)$value); // If $value is an array, make it a CSV (unlikely)
    if(get_post_meta($post->ID, $key, FALSE)) { // If the custom field already has a value
    update_post_meta($post->ID, $key, $value);
    } else { // If the custom field doesn't have a value
    add_post_meta($post->ID, $key, $value);
    }
    if(!$value) delete_post_meta($post->ID, $key); // Delete if blank
    }

    }

    add_action('save_post', 'wpt_save_events_meta', 1, 2); // save the custom fields

    }

    ?>

  28. Nick Albert Calib-og

    Hey devin i wanna thank you for your easy to catch demo about adding metabox. in your above example you were able to add another field and was also able to save it. my problem is how am i going to save data if i add another field but this time a select/dropdown html field?
    pls do reply.. tnx

    data1
    data2
    data3

Leave a Reply