Introduction to Custom Metaboxes

Overview

What is metadata?
Metadata is information about an object (like a post) and consists of a key and a value. For instance, when you use a page template, the name of the template used by the page is stored as metadata using the key _wp_page_template. You can add your own metadata to store anything you’d like.

This article focuses on post meta, which is metadata attached to a post of any content type (post, page, or custom post type). There is also user meta (attached to users) and term meta (attached to taxonomy terms, like categories and tags).

What is a metabox?
A metabox is simply a box that lets you edit metadata. While WordPress already includes a simple “Custom Fields” metabox (screenshot), it’s best to build your own metabox, which will look better and be easier to use.

A common use for metaboxes is to store structured data that is then displayed by the theme. This allows you to separate the data entry from visual presentation. Instead of trying to get the markup just right on a pricing table, create a “Pricing Table” metabox to collect the fields and let the theme take care of the markup.

How to use meta in your theme?
WordPress has a simple function for accessing post meta: get_post_meta( $post_id, $key, true );. See the WordPress Code Reference for more information on its usage.

Depending on the plugin you use and the complexity of the fields you build, you may choose to use plugin-specific functions to access metadata. For instance, with Carbon Fields you can use carbon_get_post_meta( $post_id, $key );. Always first check that the function exists( if( function_exists( 'carbon_get_post_meta' ) ) ) so your site doesn’t break if the plugin is deactivated.

Creating Metaboxes with Plugins

The simplest way to create a metabox is to use a “metabox plugin” to do the heavy lifting. You describe the fields you want and the plugin takes care of actually building the metabox and saving the data.

If you will be publicly distributing a plugin, it’s best to build the metabox directly in the plugin itself rather than require a second plugin to be installed. Here are two examples of plugins that use custom metaboxes:

I’ll walk you through building the metabox for this page using three popular metabox plugins.

Advanced Custom Fields

Advanced Custom Fields is the most popular plugin for creating metaboxes. You can easily build a metabox from the WordPress backend with no code at all.  You can use the Local JSON feature to save your metabox settings in your theme so it can be version controlled. While the free version of the plugin includes many basic field types, this tutorial uses the “Repeater” field which is only available in ACF Pro.

In the backend of your site, go to Custom Fields > Field Groups and click “Add New”. Give your metabox a name at the top, then click “Add Field” to add your fields. Here is a list of the available field types.

I’m adding a repeater field, and inside of that I’m adding some text, textarea, and checkbox fields. I’m also setting the “width” on the first 3 fields to 33% so they appear in three columns.

In the “Location” section you can set rules that determine where this metabox appears. I’m limiting this metabox to appear on pages using the “Plugins (ACF)” template.

To access the data in your theme, you can either use the WordPress core function get_post_meta() or the ACF functions. I’m using the ACF functions in the template file below. Here’s the raw metadata stored in the database.

To keep the example simple and easily comparable between plugins, I’m putting all the data into an array and passing it to a function that builds the plugin listing.a

<?php
/**
* Template Name: Plugins (ACF)
*
* @package BE2018
* @author Bill Erickson
* @since 1.0.0
* @license GPL-2.0+
**/
// General functions used by all plugin templates
require get_template_directory() . '/inc/plugins-general.php';
/**
* Plugin Listing
*
*/
function ea_acf_fields_plugin_listing() {
if( function_exists( 'have_rows' ) && have_rows( 'be_plugins' ) ):
$plugins = array();
while( have_rows( 'be_plugins' ) ): the_row();
$plugins[] = array(
'name' => get_sub_field( 'name' ),
'summary' => get_sub_field( 'summary' ),
'url' => get_sub_field( 'url' ),
'icon' => get_sub_field( 'icon' ),
'categories' => get_sub_field( 'categories' ),
);
endwhile;
ea_plugins_listing( $plugins );
endif;
}
add_action( 'tha_content_loop', 'ea_acf_fields_plugin_listing' );
remove_action( 'tha_content_loop', 'ea_default_loop' );
// Build the page
require get_template_directory() . '/index.php';
view raw plugins-acf.php hosted with ❤ by GitHub

Carbon Fields

Carbon Fields is another great metabox tool. You build metaboxes using code rather than a UI like AC. The code is modern and easy to read, and the resulting metaboxes look great. It takes the best from both ACF and CMB2 and improves upon it. They are also working on a block builder for the new block editor.

Installing the plugin is more difficult than the others. While there is a Carbon Fields plugin in the WordPress plugin repository, it’s not up-to-date. They made non-backwards-compatible changes and decided not to update the plugin repo. You can either use Composer or download the plugin here.

Once installed and active, create carbon-fields.php file in your core functionality plugin (preferred) or theme. See the documentation for more information on how to build a metabox.

<?php
/**
* Custom Fields with Carbon Fields
*
* @package CoreFunctionality
* @author Bill Erickson
* @since 1.0.0
* @license GPL-2.0+
**/
use Carbon_Fields\Container;
use Carbon_Fields\Field;
/**
* Register Fields
*
*/
function ea_register_custom_fields() {
Container::make( 'post_meta', 'Plugins' )
->where( 'post_type', '=', 'page' )
->where( 'post_template', '=', 'templates/plugins-carbonfields.php' )
->add_fields( array(
Field::make( 'complex', 'be_plugins', 'Plugins' )
->setup_labels( array( 'singular_name' => 'Plugin', 'plural_name' => 'Plugins' ) )
->set_collapsed( true )
->add_fields( array(
Field::make( 'text', 'name' )->set_width(33),
Field::make( 'text', 'url', 'URL' )->set_width(33),
Field::make( 'text', 'icon' )->set_width(33),
Field::make( 'textarea', 'summary' ),
Field::make( 'set', 'categories' )
->set_options( 'ea_plugin_categories' )
))
->set_header_template( '<% if (name) { %><%- name %><% } %>' )
));
}
add_action( 'carbon_fields_register_fields', 'ea_register_custom_fields' );
/**
* Plugin Categories
*
*/
function ea_plugin_categories() {
return array(
'genesis' => 'Genesis',
'developer' => 'Developer',
'cms' => 'CMS',
'social' => 'Social',
'media' => 'Media',
'widget' => 'Widget',
'analytics' => 'Analytics'
);
}

I have it set to ->set_collapsed( true ) so when you load the page you only see the small header bars (like “Genesis Title Toggle” below), but in this screenshot I’ve expanded the second entry.

I’m using the plugin functions for accessing the metadata. In case you’re curious, here’s the raw metadata that’s saved to the database. It isn’t as easy to follow as ACF or CMB2 so I definitely recommend using the plugin functions. While it is all stored as separate meta, on “complex” fields carbon_get_post_meta() will return a single array containing all the data.

Here’s the template file:

<?php
/**
* Template Name: Plugins (Carbon Fields)
*
* @package BE2018
* @author Bill Erickson
* @since 1.0.0
* @license GPL-2.0+
**/
// General functions used by all plugin templates
require get_template_directory() . '/inc/plugins-general.php';
/**
* Plugin Listing
*
*/
function ea_carbon_fields_fields_plugin_listing() {
if( function_exists( 'carbon_get_post_meta' ) ) {
$plugins = carbon_get_post_meta( get_the_ID(), 'be_plugins' );
ea_plugins_listing( $plugins );
}
}
add_action( 'tha_content_loop', 'ea_carbon_fields_fields_plugin_listing' );
remove_action( 'tha_content_loop', 'ea_default_loop' );
// Build the page
require get_template_directory() . '/index.php';

CMB2

CMB2 is the most developer focused of these three plugins. It’s infinitely extensible and often used in large projects since every aspect can be customized, including custom styles for the metabox. But it can be a bit difficult to learn and doesn’t look as good out of the box as the other plugins I’ve covered.

You can either use the WordPress plugin (as I have done) or include the CMB2 code directly in your theme/plugin. It’s smart enough to only load once when multiple copies are enqueued, and always select the most recent version.

I recommend you review the documentation on Basic UsageField TypesField Parameters, and Display Options.

In my core functionality plugin I created cmb2.php and added the following:

<?php
/**
* Custom Fields with CMB2
*
* @package CoreFunctionality
* @author Bill Erickson
* @since 1.0.0
* @license GPL-2.0+
**/
/**
* Define the metabox and field configurations.
*/
function be_register_cmb2_metaboxes() {
/**
* Initiate the metabox
*/
$cmb = new_cmb2_box( array(
'id' => 'be_plugins_metabox',
'title' => __( 'Plugins' ),
'object_types' => array( 'page' ),
'show_on' => array( 'key' => 'page-template', 'value' => 'templates/plugins-cmb2.php' ),
'context' => 'normal',
'priority' => 'high',
'show_names' => true, // Show field names on the left
// 'cmb_styles' => false, // false to disable the CMB stylesheet
// 'closed' => true, // Keep the metabox closed by default
) );
$group_field_id = $cmb->add_field( array(
'id' => 'be_plugins',
'type' => 'group',
'options' => array(
'group_title' => __( 'Plugin {#}' ),
'add_button' => __( 'Add Another Plugin' ),
'remove_button' => __( 'Remove Plugin' ),
'sortable' => true,
)
));
$cmb->add_group_field( $group_field_id, array(
'name' => 'Name',
'id' => 'name',
'type' => 'text',
));
$cmb->add_group_field( $group_field_id, array(
'name' => 'URL',
'id' => 'url',
'type' => 'text',
));
$cmb->add_group_field( $group_field_id, array(
'name' => 'Icon',
'id' => 'icon',
'type' => 'text',
));
$cmb->add_group_field( $group_field_id, array(
'name' => 'Summary',
'id' => 'summary',
'type' => 'textarea',
));
$cmb->add_group_field( $group_field_id, array(
'name' => 'Categories',
'id' => 'categories',
'type' => 'multicheck_inline',
'options' => ea_plugin_categories(),
));
}
add_action( 'cmb2_admin_init', 'be_register_cmb2_metaboxes' );
/**
* Plugin Categories
*
*/
function ea_plugin_categories() {
return array(
'genesis' => 'Genesis',
'developer' => 'Developer',
'cms' => 'CMS',
'social' => 'Social',
'media' => 'Media',
'widget' => 'Widget',
'analytics' => 'Analytics'
);
}
view raw cmb2.php hosted with ❤ by GitHub

Here’s the page template with the metabox:

CMB2 uses the standard WordPress function get_post_meta() for retrieving meta, so there are no plugin-specific functions you need to learn.

Because we are using the “Group” field type for repeatable fields, it stores all of that data in a single post_meta field as a serialized array (see the raw metadata here), which WordPress unserializes into a standard array. Here’s the page template code:

<?php
/**
* Template Name: Plugins (CMB2)
*
* @package BE2018
* @author Bill Erickson
* @since 1.0.0
* @license GPL-2.0+
**/
// General functions used by all plugin templates
require get_template_directory() . '/inc/plugins-general.php';
/**
* Plugin Listing
*
*/
function ea_cmb2_fields_plugin_listing() {
$plugins = get_post_meta( get_the_ID(), 'be_plugins', true );
ea_plugins_listing( $plugins );
}
add_action( 'tha_content_loop', 'ea_cmb2_fields_plugin_listing' );
remove_action( 'tha_content_loop', 'ea_default_loop' );
// Build the page
require get_template_directory() . '/index.php';

Bill Erickson

Bill Erickson is the co-founder and lead developer at CultivateWP, a WordPress agency focusing on high performance sites for web publishers.

About Me
Ready to upgrade your website?

I build custom WordPress websites that look great and are easy to manage.

Let's Talk

Reader Interactions

Comments are closed. Continue the conversation with me on Twitter: @billerickson

Comments

  1. alex says

    Thanks for the great script Bill. I can’t get shortcodes to work in WYSIWYG, they do if I use in WP text editor but not in additional editors. Is that something I do wrong or it’s not really possible ?

    • Bill Erickson says

      Shortcodes are only executed on the post editor because there’s a filter that runs on it. For anything else (like your custom fields) you’ll need to wrap it in do_shortcode( ). Let’s say you created a custom field called “be_extra_content” and wanted shortcodes to be executed on it.


      global $post;
      // Get the field
      $extra = get_post_meta( $post->ID, 'be_extra_content', true );
      // Filter it for shortcodes
      $extra = do_shortcode( $extra );
      // Show the content
      echo $extra;

  2. Steve says

    Love the library! Quick question for you:

    I need to store loads of events as custom post types, planning on using the date picker. Any way within your guys’ code to format the date to timestamp (i.e. 20110916) rather than 09/16/2011? How would you approach this using your custom metaboxes lib?

    • Bill Erickson says

      I use the metabox code for event post types all the time. Use the ‘text_date_timestamp’ field type because it will store it as a UNIX timestamp. Makes event queries easy

  3. mampranx says

    How to add default radio checked?

    array(
    ‘name’ => ‘Test Radio inline’,
    ‘desc’ => ‘field description (optional)’,
    ‘id’ => $prefix . ‘test_radio’,
    ‘type’ => ‘radio_inline’,
    ‘std’ => ‘ ??? ‘
    ‘options’ => array(
    array(‘name’ => ‘Option One’, ‘value’ => ‘standard’),
    array(‘name’ => ‘Option Two’, ‘value’ => ‘custom’),
    array(‘name’ => ‘Option Three’, ‘value’ => ‘none’)
    )
    ),

  4. Alex Barber says

    Here’s an out there implementation question. I’m trying to use a bit of custom code that someone wrote to make a call to Google Maps API based on a custom address field. It parses returned XML to a lat/long pair and writes it to appropriate custom fields. This code uses “publish_post” while your code uses “save_post” to update custom field values and the lat/long fields never update with the returned values. Any thoughts on how I can get the two to peacefully co-exist?

    • Bill Erickson says

      You’ll probably need to modify one or the other. As shown here, publish_post runs before save_post. Maybe you could try changing the custom code to use ‘save_post’ and a later priority (like 15).

      • Alex Barber says

        That seems to have worked. I was trying save_post and publish_post yesterday, but I wasn’t playing with priorities. I set the Google code to run with save_post with a priority of 15 and it populates the fields now.

  5. Jon says

    Bill,

    I have the following in my functions.php

    [removed by Bill]

    but the metabox does not display on the custom post type’s creation page. I’ve tried adding global $post before $prefix and that did nothing. I’m at a loss. Any thoughts? Thanks.

    I’m testing this on 3.3 alpha in a Genesis child theme.

    • Bill Erickson says

      The code that I had posted here on this page was out of date. The metabox code now uses a filter. See the updated code above, or refer to the example-functions.php file in github.

  6. alex says

    Bill, thanks again for the great framework (I use it in almost every project). Is there any way to add text fields (or any other) dynamically inside page editor just like widgets? For example I need to create flexible box so user can add as many items as they want ( so the number of inputs is different for each page ). thx

    • Bill Erickson says

      It sounds like the easiest method would be to use the Custom Fields functionality in WordPress. It lets you add as many fields as you want. When creating a metabox you have to define them all upfront in code – the client can’t add new ones on specific pages.

  7. Artin H. says

    Hey Bill,
    Is there anyway, I can have different types of meta boxes for each page.
    Lets say I have the following pages: Home, Contact, About
    Can i have different meta boxes for Home and different meta boxes for Contact and About, even though the post type for all three is array(‘page’) ?

    Thanks