Create WordPress Settings Page With Meta Boxes

In this tutorial I want to explain Step by Step How To Create WordPress Settings/Options Page With Meta Box, like what you see in this screenshot:

WordPress Settings With Meta Box
WordPress Settings With Meta Box

WordPress have a decent Settings API and it offer a lot flexibility in design. Several plugins do “wild” things in their Settings Page, However for better user experience it’s best to use seamless design (blended) with other admin UI design.

One of my favorite admin UI element is Meta Box. Not only because meta box have an easy to use Meta Box API (so we can easily create meta boxes), but it also have user preference options where user can reorder (drag-and-drop) the position, toggle open/close meta boxes. and even changing Screen Layout to 1 or 2 column using “Screen Options”.

Benefit in using Meta Box in Settings Page:

  1. Nice UI : Neatly Group Complex Settings.
  2. Minimum Design Time : WordPress already have the design.
  3. Easy to use : because user already familiar with how the panel works.
  4. Extend-Ability : Other developer can easily extend our plugins and add options with familiar API.

So Let’s Start !

Wait, before we start, maybe it’s better if you download the example plugin to easily follow this tutorial. I host it at Github. So, fork if you must 🙂

In this tutorial, I will also assume that you already familiar with WordPress Settings API and WordPress Meta Box API.

Prepare Our Plugin

Create the plugin folder, and create simple plugin header to start.

/**
 * Plugin Name: f(x) Settings Meta Box Example
 * Plugin URI: https://shellcreeper.com/wp-settings-meta-box/
 * Description: Code Example for Settings Page with Meta Box. 
 * Version: 0.1.0
 * Author: David Chandra Purnama
 * Author URI: https://shellcreeper.com/
**/

Create Blank Settings Page

/* === SETTINGS PAGE === */

/* Add Settings Page */
add_action( 'admin_menu', 'fx_smb_settings_setup' );

/**
 * Create Settings Page
 * @since 0.1.0
 * @link http://codex.wordpress.org/Function_Reference/register_setting
 * @link http://codex.wordpress.org/Function_Reference/add_menu_page
 * @uses fx_smb_setings_page_id()
 */
function fx_smb_settings_setup(){

    /* Register our setting. */
    register_setting(
        'fx_smb',                         /* Option Group */
        'fx_smb_basic',                   /* Option Name */
        'fx_smb_basic_sanitize'           /* Sanitize Callback */
    );

    /* Add settings menu page */
    $settings_page = add_menu_page(
        'f(x) Settings Meta Box Example', /* Page Title */
        'Meta Box',                       /* Menu Title */
        'manage_options',                 /* Capability */
        'fx_smb',                         /* Page Slug */
        'fx_smb_settings_page',           /* Settings Page Function Callback */
        'dashicons-align-left',           /* Menu Icon */
        5                                 /* Menu Position */
    );

    /* Vars */
    $page_hook_id = fx_smb_setings_page_id();

    /* Do stuff in settings page, such as adding scripts, etc. */
    if ( !empty( $settings_page ) ) {

        /* We will add several stuff here */

    }
}

/**
 * Utility: Settings Page Hook ID
 * The Settings Page Hook, it's the same with global $hook_suffix.
 * @since 0.1.0
 */
function fx_smb_setings_page_id(){
    return 'toplevel_page_fx_smb';
}

/**
 * Settings Page Callback
 * used in fx_smb_settings_setup().
 * @since 0.1.0
 */
function fx_smb_settings_page(){

    /* We will add our settings HTML here. */

}

The function fx_smb_settings_setup() is to register our settings and add settings in admin menu. And we will use the utility function fx_smb_setings_page_id() to refer our settings page, for example to enqueue scripts.

I use admin menu name “Meta Box” just because (no reason) and add in menu position 5, which is after the “Dashboard” Menu to make it easier to display it in the screenshot 🙂

After we have this function in place we will have a blank settings page ready.

Create Settings Page

In our (currently blank) fx_smb_settings_page() function we will add our HTML markup for the settings and use the “Post Edit Screen” Markup.

You can check “wp-admin/edit-form-advanced.php” if you  want to learn more about the HTML structure and functions. I simplify the markup in post edit screen and only use what we need.

/**
 * Settings Page Callback
 * used in fx_smb_settings_setup().
 * @since 0.1.0
 */
function fx_smb_settings_page(){

    /* global vars */
    global $hook_suffix;

    /* utility hook */
    do_action( 'fx_smb_settings_page_init' );

    /* enable add_meta_boxes function in this page. */
    do_action( 'add_meta_boxes', $hook_suffix );
    ?>

    <div class="wrap">

        <h2>Settings Meta Box</h2>

        <?php settings_errors(); ?>

        <div class="fx-settings-meta-box-wrap">

            <form id="fx-smb-form" method="post" action="options.php">

                <?php settings_fields( 'fx_smb' ); // options group  ?>
                <?php wp_nonce_field( 'closedpostboxes', 'closedpostboxesnonce', false ); ?>
                <?php wp_nonce_field( 'meta-box-order', 'meta-box-order-nonce', false ); ?>

                <div id="poststuff">

                    <div id="post-body" class="metabox-holder columns-<?php echo 1 == get_current_screen()->get_columns() ? '1' : '2'; ?>">

                        <div id="postbox-container-1" class="postbox-container">

                            <?php do_meta_boxes( $hook_suffix, 'side', null ); ?>
                            <!-- #side-sortables -->

                        </div><!-- #postbox-container-1 -->

                        <div id="postbox-container-2" class="postbox-container">

                            <?php do_meta_boxes( $hook_suffix, 'normal', null ); ?>
                            <!-- #normal-sortables -->

                            <?php do_meta_boxes( $hook_suffix, 'advanced', null ); ?>
                            <!-- #advanced-sortables -->

                        </div><!-- #postbox-container-2 -->

                    </div><!-- #post-body -->

                    <br class="clear">

                </div><!-- #poststuff -->

            </form>

        </div><!-- .fx-settings-meta-box-wrap -->

    </div><!-- .wrap -->
    <?php
}

After we add this code in our function we will still see the empty settings page, but now we have a settings title, “Settings Meta Box” (in H2) at the top of our settings page.

Use Settings API Functions

As you can see above, I use several function in Settings API you already familiar with:

  1. settings_errors() : this is to display “Updated” and “Error” message when we save or reset our settings.
  2. settings_fields(): this is needed in Settings API, this is where WordPress handle settings we register to this page using register_setting() function.

But we are not using add_settings_field() and add_settings_section() function, because we are going to add our settings using Meta Box API.

Enable Meta Box in Settings Page

So to replace add_settings_field() and add_settings_section() functionality, we need to enable Meta Boxes API in our settings, and to do this we use do_meta_boxes() function:

do_meta_boxes( $page, $context, $object );

There’s three instance of do_meta_boxes() for each meta box context / location ( “side”, “normal”, and “advance” ), and we use global $hook_suffix as the page identifier.

Usually when we add our meta box in Post Edit Screen, we define the page using post type as identifier ( “post”, “page”, or “our-cpt” ), and now if we want to add meta boxes in this settings page, we simply change the page with our settings page $hook_suffix. and it’s the same return value with our utility function fx_smb_setings_page_id().

The function do_meta_boxes() will do nothing, To enable it we need to add add_meta_boxes action hook, as you see we have this code: do_action( 'add_meta_boxes', $hook_suffix ); at the top of the page.

We also add an utility hook fx_smb_settings_page_init at the top of the page and we will use this later.

Enable Meta Boxes Functionality

Adding meta box is not enough, we also need to enable Meta Box functionality, such as “reorder meta box” using drag and drop, “toggle meta box“, and change the “screen layout“. This functionality require several script, and we need to add these scripts in our Settings Page.

Let’s back to fx_smb_settings_setup() function to add scripts needed and set number of available screen layout column.

    /* Vars */
    $page_hook_id = fx_smb_setings_page_id();

    /* Do stuff in settings page, such as adding scripts, etc. */
    if ( !empty( $settings_page ) ) {

        /* Load the JavaScript needed for the settings screen. */
        add_action( 'admin_enqueue_scripts', 'fx_smb_enqueue_scripts' );
        add_action( "admin_footer-{$page_hook_id}", 'fx_smb_footer_scripts' );

        /* Set number of column available. */
        add_filter( 'screen_layout_columns', 'fx_smb_screen_layout_column', 10, 2 );

    }

and the functions:

/**
 * Load Script Needed For Meta Box
 * @since 0.1.0
 */
function fx_smb_enqueue_scripts( $hook_suffix ){
    $page_hook_id = fx_smb_setings_page_id();
    if ( $hook_suffix == $page_hook_id ){
        wp_enqueue_script( 'common' );
        wp_enqueue_script( 'wp-lists' );
        wp_enqueue_script( 'postbox' );
    }
}

/**
 * Footer Script Needed for Meta Box:
 * - Meta Box Toggle.
 * @since 0.1.0
 */
function fx_smb_footer_scripts(){
    $page_hook_id = fx_smb_setings_page_id();
?>
<script type="text/javascript">
    //<![CDATA[
    jQuery(document).ready( function($) {
        // toggle
        $('.if-js-closed').removeClass('if-js-closed').addClass('closed');
        postboxes.add_postbox_toggles( '<?php echo $page_hook_id; ?>' );
    });
    //]]>
</script>
<?php
}

/**
 * Number of Column available in Settings Page.
 * we can only set to 1 or 2 column.
 * @since 0.1.0
 */
function fx_smb_screen_layout_column( $columns, $screen ){
    $page_hook_id = fx_smb_setings_page_id();
    if ( $screen == $page_hook_id )
        $columns[$page_hook_id] = 2;
    return $columns;
}

To save this user preference in user meta, we need to add nonce in our settings page, If you check fx_smb_settings_page() function there’s two nonce field added, one for meta box toggle, and one for meta box order. We need to add it inside the form, and I add it after settings_fields( 'fx_smb' ); function:

<?php wp_nonce_field( 'closedpostboxes', 'closedpostboxesnonce', false ); ?>
<?php wp_nonce_field( 'meta-box-order', 'meta-box-order-nonce', false ); ?>

And after we add that, we will see a working meta box area with screen layout option enabled.

Meta Box Ready Settings Page
Meta Box Ready Settings Page

Note: WordPress only add border to “Side” Meta Box Area (context), And no “screen layout” selected yet.

Create Save Options Meta Box

In Post Edit Screen, we have “Publish” meta box, and we are going to add similar meta box to Save our options.

/* === SUBMIT / SAVE META BOX === */

/* Add Meta Box */
add_action( 'add_meta_boxes', 'fx_smb_submit_add_meta_box' );

/**
 * Add Submit/Save Meta Box
 * @since 0.1.0
 * @uses fx_smb_submit_meta_box()
 * @link http://codex.wordpress.org/Function_Reference/add_meta_box
 */
function fx_smb_submit_add_meta_box(){

    $page_hook_id = fx_smb_setings_page_id();

    add_meta_box(
        'submitdiv',               /* Meta Box ID */
        'Save Options',            /* Title */
        'fx_smb_submit_meta_box',  /* Function Callback */
        $page_hook_id,                /* Screen: Our Settings Page */
        'side',                    /* Context */
        'high'                     /* Priority */
    );
}

/**
 * Submit Meta Box Callback
 * @since 0.1.0
 */
function fx_smb_submit_meta_box(){

    /* Reset URL */
    $reset_url = '#';

?>
<div id="submitpost" class="submitbox">

    <div id="major-publishing-actions">

        <div id="delete-action">
            <a href="<?php echo esc_url( $reset_url ); ?>" class="submitdelete deletion">Reset Settings</a>
        </div><!-- #delete-action -->

        <div id="publishing-action">
            <span class="spinner"></span>
            <?php submit_button( esc_attr( 'Save' ), 'primary', 'submit', false );?>
        </div>

        <div class="clear"></div>

    </div><!-- #major-publishing-actions -->

</div><!-- #submitpost -->

<?php
}

As you can see, we use “submitdiv” as Meta Box ID, this is the same ID as “Publish” Meta Box we see in Post Edit Screen, We also reuse HTML elements in “Publish” Meta Box. This provide several benefit:

  1. No need to style it, it will use the same design as “Publish” Meta Box.
  2. It’s automatically excluded in “Show on screen” check boxes in “Screen Options” toggle, so user can’t hide this meta box.

The “Save” Option functionality is working at this point, because WordPress Settings API have this build-in, so the “Save” button will work. But the “Reset Settings” link is not yet working. We need to build this functionality our-self, because WordPress do not have this functionality. This feature is optional,

    /* Reset URL */
    $reset_url = add_query_arg( array(
            'page' => 'fx_smb',
            'action' => 'reset_settings',
            '_wpnonce' => wp_create_nonce( 'fx-smb-reset', __FILE__ ),
        ),
        admin_url( 'admin.php' )
    );

We use add_query_arg() function to build URL structure for “Reset Settings” link, and the out put will be:

http://site.com/wp-admin/admin.php?page=fx_smb&action=reset_settings&_wpnonce=788dc6037e

So it’s just the URL to our settings page with additional parameter to reset settings and nonce to make it secure. This URL still do nothing, To actually reset the settings we are going to use our utility hook  “fx_smb_settings_page_init” in fx_smb_settings_page() function:

 /* Reset Settings */
add_action( 'fx_smb_settings_page_init', 'fx_smb_reset_settings' );

/**
 * Delete Options
 * @since 0.1.0
 */
function fx_smb_reset_settings(){

    /* Check Action */
    $action = isset( $_REQUEST['action'] ) ? $_REQUEST['action'] : '';
    if( 'reset_settings' == $action ){

        /* Check User Capability */
        if( current_user_can( 'manage_options' ) ){

            /* nonce */
            $nonce = isset( $_REQUEST['_wpnonce'] ) ? $_REQUEST['_wpnonce'] : '';

            /* valid */
            if( wp_verify_nonce( $nonce, 'fx-smb-reset' ) ){

                /* Delete Option */
                delete_option( 'fx_smb_basic' );

                /* Utility hook. */
                do_action( 'fx_smb_reset' );

                /* Add Update Notice */
                add_settings_error( "fx_smb", "", "Settings reset to defaults.", 'updated' );
            }
            /* not valid */
            else{
                /* Add Error Notice */
                add_settings_error( "fx_smb", "", "Failed to reset settings. Please try again.", 'error' );
            }
        }
        /* User Do Not Have Capability */
        else{
            /* Add Error Notice */
            add_settings_error( "fx_smb", "", "Failed to reset settings. You do not capability to do this action.", 'error' );
        }
    }
}

It’s a straightforward function:

  1. Check “reset_settings” action request.
  2. Check the user capability
  3. Verify our nonce,
  4. If everything is good, delete our option and display “Updated” notification.
  5. If something goes wrong, do nothing and display “Error” notification.

We use delete_option() instead of saving the default option in database. I consider this is important to actually remove the entry from database instead of saving the default because:

  1. Cleaner database.
  2. Use sane defaults in our function.

I explain this because there’s a popular settings framework doing it wrong and save the default option instead of deleting the option in their “reset” functionality.

Fine-tuning “Save Options” Meta Box:

If you check in our “Save Options” meta box function fx_smb_submit_meta_box(). Above the submit button, there’s a “spinner” empty span. This span as default is hidden using CSS, and when we submit our form, we need to display it. This is to notify user that the submit button is working and processing the data. We also need to create a pop-up confirmation if user want to reset the settings. We can add  simple javascript to do this in fx_smb_footer_scripts().

// display spinner
$('#fx-smb-form').submit( function(){
    $('#publishing-action .spinner').css('display','inline');
});
// confirm before reset
$('#delete-action .submitdelete').on('click', function() {
    return confirm('Are you sure want to do this?');
});

And now we have our “Save Option” meta box working.

Working Spinner and Reset Confirmation Box
Working Spinner and Reset Confirmation Box

Create Our Meta Box Settings

Now, we can create our Settings input using Meta Box API. It’s just simple example and we only create one text field.

But first, let’s back to fx_smb_settings_setup() function where we register our setting.

    /* Register our setting. */
    register_setting(
        'fx_smb',                         /* Option Group */
        'fx_smb_basic',                   /* Option Name */
        'fx_smb_basic_sanitize'           /* Sanitize Callback */
    );

So we are going to save our option in “fx_smb_basic”, and the function we use to sanitize the option is “fx_smb_basic_sanitize”.

With this data, Let’s create options!

/* === EXAMPLE BASIC META BOX === */

/* Add Meta Box */
add_action( 'add_meta_boxes', 'fx_smb_basic_add_meta_box' );

/**
 * Basic Meta Box
 * @since 0.1.0
 * @link http://codex.wordpress.org/Function_Reference/add_meta_box
 */
function fx_smb_basic_add_meta_box(){

    $page_hook_id = fx_smb_setings_page_id();

    add_meta_box(
        'basic',                  /* Meta Box ID */
        'Meta Box',               /* Title */
        'fx_smb_basic_meta_box',  /* Function Callback */
        $page_hook_id,               /* Screen: Our Settings Page */
        'normal',                 /* Context */
        'default'                 /* Priority */
    );
}

/**
 * Submit Meta Box Callback
 * @since 0.1.0
 */
function fx_smb_basic_meta_box(){
?>
<?php /* Simple Text Input Example */ ?>
<p>
    <label for="basic-text">Basic Text Input</label>
    <input id="basic-text" class="widefat" type="text" name="fx_smb_basic" value="<?php echo sanitize_text_field( get_option( 'fx_smb_basic', '' ) );?>">
</p>
<p class="howto">To display this option use PHP code<code>get_option( 'fx_smb_basic' );</code>.</p>
<?php
}

/**
 * Sanitize Basic Settings
 * This function is defined in register_setting().
 * @since 0.1.0
 */
function fx_smb_basic_sanitize( $settings  ){
    $settings = sanitize_text_field( $settings );
    return $settings ;
}

Very simple right?

In the example I only create one text field, and it is saved in “fx_smb_basic” option name. If you want to create several field in the meta box you can also save them in single data entry as array, This can make the database leaner.

Database Schema Notes:

If you need more than one meta boxes, It is better to use register_setting() for each meta boxes. That means, save each meta box data in different option_name.

It will offer more flexibility when we need to create / remove additional meta boxes/settings.

If you save all meta boxes data in one data entry and In the future you need to remove additional meta boxes, when user save the data, all previous data will be lost (sometimes this is not wanted). This will also make the database structure more manageable.

Example use case: If we create an add-on plugin for the “main” plugin. And this add-on create additional meta box options.

Design Notes:

If you want to prettify the meta box design, always remember:

  1. WordPress Admin is Responsive, make sure user can easily input the data using small screen device.
  2. User can change meta box order, and drag the meta box in “Side” location (context). Make sure all field look neat there too.
  3. Check out WordPress default meta boxes, there’s several HTML class you can use, for example “howto” class for explaining a field.

Well, that’s it. If you have questions or suggestions, leave a comment. If you think it will be useful for others, share 🙂

9 Comments

  1. Dreb Bits

    I appreciate your effort in putting this seamless tutorial together. A question though, how do you approach validating fields that are required (not sanitizing)?

  2. Dan

    Hi David,

    Great tutorial! I was wandering if you could help with adding and saving an array of checkboxes?

    Thanks,
    Dan

  3. Brad

    I have a custom post type in which I added a couple of meta boxes. I’m having difficulty figuring out how to save the information all the custom post information. I’m relatively new to WordPress. Any advice on how to get that figured out? This is what I have in the custom plugin…

    function add_location_meta_box() {
    add_meta_box(‘location-information’, ‘Location Information’, ‘location_options’, ‘cpt’, ‘normal’, ‘default’);
    }

    function add_other_meta_box() {
    add_meta_box(‘other-information’, ‘Other’, ‘other_options’, ‘cpt’, ‘normal’, ‘default’);
    }

    add_action(‘add_meta_boxes’, ‘add_location_meta_box’);
    add_action(‘add_meta_boxes’, ‘add_other_meta_box’);

  4. gollum

    // display spinner
    $(‘#fx-smb-form’).submit( function(){
    $(‘#publishing-action .spinner’).css(‘display’,’inline’);
    });

    Above jQuery snippet from your tutorial doesn’t seem to work in WordPress 4.9.4. So changed it to:

    // display spinner
    $(‘#fx-smb-form’).submit( function(){
    $(‘#publishing-action .spinner’).css(‘visibility’,’visible’);
    });

    • gollum

      Even better (as of WordPress 4.2.0 release):

      $(‘#publishing-action .spinner’).addClass(‘is-active’);

  5. Alan

    Nice tutorial, however the reset option logic doesn’t work well, as the nonce and reset query args stay on the url, so if you reset and then change settings, they reset ( when that is what is not intended )

    Ben thinking of ways around it, I think teh way to do it is with another option (reset) and pick this up in say sanitise ..

Comments are closed.