Incorporating the Settings API in WordPress Themes

Filed in Web DevelopmentTags: Settings API, Themes, tutorials, WordPress

The inter-tubes are awash in tutorials for how to add Theme Options to WordPress Themes - so why write another? Primarily, because most such tutorials are several years old, don't implement current best-practices, and were written without any awareness of the WordPress Settings API.

While others such as Otto and Ozh have done yeomen's work in explaining how to implement the Settings API, I have not yet come across anything that really put everything together, and explained the process and implementation from beginning to end, in a way that even the less-experienced Theme developers (like me) could easily understand.

This tutorial will attempt to fill that gap, by providing examples of current (as of the pending release of WordPress 3.1) best-practice implementation, not merely of the Settings API, but of Theme Options implementation as a whole, including:

  • Registering options in the database as a single options array
  • Initializing default options
  • Creating a single Theme Settings page (with tabs)
  • Defining settings page sections and fields
  • Validating and white-listing user-input form data
  • Adding Settings Page contextual help
  • Enqueueing custom CSS for the Settings page
  • Implementing settings in the Theme template files
  • Enqueueing front-end CSS

Throughout this tutorial, I will be using code from my Oenology Theme for implementation examples. For full code, see the latest development version.

Assumptions: Best Practices

The following will be the working list of best practices that will be incorporated:

  • Theme Settings defined as an options array in a single database entry
  • Theme settings added to a single Theme Settings Page
  • Theme Settings page is added to the "Appearance" menu
  • Theme Settings page added using the "edit_theme_options" user capability
  • Theme Settings registered, updated, and validated using the WordPress Settings API
  • Theme Settings modify the template using action/filter hooks wherever possible

Getting Started

The first thing we need to do, even before touching any code, is to determine:

  • What options to include in the Theme
  • How to organize those options on the Theme Settings Page

In my case, I am adding only a handful of simple options:

  • Header Navigation Menu Position: currently, Oenology displays the Header Navigation Menu above the site title and description. I am adding a setting optionally to display the Header Navigation Menu below the site title and description.
  • Header Navigation Menu Depth: currently, Oenology is designed so that only the top-level Pages display in the Header Navigation Menu, and Child Pages display in a left-column sub-menu. I am adding a setting optionally to display Pages to a depth of one (top-level Pages only), two, or three, with hover drop-down menus.
  • Footer Credit Link: currently, Oenology does not display any form of footer credit link. I am adding a setting optionally to display a footer credit link.
  • Varietals: the default style of Oenology is intentionally minimal. It is intended to be clean, simple, and cross-browser. I am adding a setting optionally to select from among Theme "skins" (which, in keeping with the oenology metaphor, will be called "varietals"), which will apply different color/style schemes to the Theme.

Given that I'm only adding four Theme options, I could very easily put all four together on one Settings page. However, I may want to add additional options in the future - and also, I want to provide a proof-of-concept for creating complex Theme Settings pages in a way that supports the Settings API. So, the Theme Settings page will have two tabs: "General", and "Varietals". Further, the "General" tab will have two sections: "Header Options" and "Footer Options".

So, that's our basis. Let's get started!

Define/Initialize Default Options

Define Defaults

The first step is simple: define our setting defaults. Since the options will be added to the database as an array, we will also define our defaults as an array (this array will be wrapped in a function, to facilitate reuse):

function oenology_get_default_options() {
     $options = array(
          'header_nav_menu_position' => 'top',
          'header_nav_menu_depth' => 1,
          'display_footer_credit' => false,
          'varietal' => 'cuvee',
          'theme_version' => '1.1'
     );
     return $options;
}

In this case, the default settings will retain the existing behavior and appearance of Oenology. The Header Navigation Menu will display above the site title/description, and will display only top-level Pages. The footer will not display a credit link. The default Varietal, which will be called "Cuvee", will retain the same Theme appearance.

(I also added in an un-exposed option, "theme_version", that might potentially be used in the future, in case I need to make any version-specific changes/updates to the Theme options.)

Add Options to Database/Initialize Default Settings

For this step, we will take advantage of the power of the update_option() function, which can be used both to update existing options in, and also to add new options to, the database:

function oenology_options_init() {
     // set options equal to defaults
     global $oenology_options;
     $oenology_options = get_option( 'theme_oenology_options' );
     if ( false === $oenology_options ) {
          $oenology_options = oenology_get_default_options();
     }
     update_option( 'theme_oenology_options', $oenology_options );
}
// Initialize Theme options
add_action('after_setup_theme','oenology_options_init', 9 );

This function simply looks to see if our defined option exists in the database; if it doesn't, it writes the default values into the option, and then adds the option to the database. This function is hooked into the after_setup_theme hook.

Helper Functions

Note: I'm also going to define some other "helper" functions, like our default options function:

Theme Varietals:
function oenology_get_valid_varietals() {
     $varietals = array(
          'cuvee' => array(
               'slug' => 'cuvee',
               'name' => 'Cuvee',
               'description' => '"Cuvee" is a term often used by wineries to describe a particularly high-quality batch of wine. Cuvee is the base style for Oenology.'
          ),
          'syrah' => array(
               'slug' => 'syrah',
               'name' => 'Syrah',
               'description' => 'Syrah is a red grape that produces a full-bodied, almost inky-black wine with a spicy, earthy flavor and aroma.'
          ),
          'seyval-blanc' => array(
               'slug' => 'seyval-blanc',
               'name' => 'Seyval Blanc',
               'description' => 'Seyval Blanc is a white grape, typically grown in cooler climates, that produces a wine with flavors of citrus and mineral.'
          )
     );
     return $varietals;
}
Settings Page Tabs:
function oenology_get_settings_page_tabs() {
     $tabs = array(
          'general' => 'General',
          'varietals' => 'Varietals'
     );
     return $tabs;
}

This information will be used repeatedly, so having these functions will facilitate that reuse.

At this point, we simply have our options array added to the database, with default values. Now the real fun begins: building the Theme Settings page!

Create Theme Settings Page

As explained in the Codex, creating a Settings page is a three-step process:

  1. Create the HTML output for the Settings page
  2. Define a function to hold that HTML output
  3. Register that function using the admin_menu hook

For ease of explanation, we'll take those steps in reverse.

Hook Settings Page Into Appearance Menu

The first step is to hook our Theme Settings page into the appropriate Admin menu. Since our Settings page applies to a Theme, the appropriate Admin menu is "Appearance", where the rest of the Theme- and appearance-related settings pages reside. For this purpose, we will use the add_theme_page() function.

// Add "Oenology Options" link to the "Appearance" menu
function oenology_menu_options() {
     add_theme_page('Oenology Options', 'Oenology Options', 'edit_theme_options', 'oenology-settings', 'oenology_admin_options_page');
}
// Load the Admin Options page
add_action('admin_menu', 'oenology_menu_options');
Function: add_theme_page()

Codex: Administration Menus

The add_theme_page( $page_title, $menu_title, $capability, $menu_slug, $function ) function accepts five arguments:

  • $page_title: the HTML page title
  • $menu_title: the title displayed in the "Appearance" menu
  • $capability: the appropriate user capability for access to the Settings page. Use edit_theme_options, not edit_themes
  • $menu_slug: the slug added to the Settings page URL, i.e. "...themes.php?page=$menu_slug"
  • $function: the callback function in which the Settings page markup is defined

We hook this function into the admin_init action hook.

Define Function to Hold HTML Output

The fifth argument in our call to add_theme_page() was 'oenology_admin_options_page', which is the name of the function that holds our Settings page markup, so we next need to define that function:

function oenology_admin_options_page() { ?>
     <-- HTML markup here -->
<?php }

Create HTML Output for Settings Page

Now for the complex part: creating the actual HTML markup for the settings page.

Settings Page Tabs

First, we need the markup (note: modified from this tutorial by Daniel Tara) for our Settings page tabs. For readability, I'm going to put this markup into its own function:

function oenology_admin_options_page_tabs( $current = 'general' ) {
     if ( isset ( $_GET['tab'] ) ) :
          $current = $_GET['tab'];
     else:
          $current = 'general';
     endif;
     $tabs = oenology_get_settings_page_tabs();
     $links = array();
     foreach( $tabs as $tab => $name ) :
          if ( $tab == $current ) :
               $links[] = "<a class="nav-tab nav-tab-active" href="?page=oenology-settings&tab=$tab">$name</a>";
          else :
               $links[] = "<a class="nav-tab" href="?page=oenology-settings&tab=$tab">$name</a>";
          endif;
     endforeach;
     echo '<div id="icon-themes" class="icon32"><br /></div>';
     echo '<h2 class="nav-tab-wrapper">';
     foreach ( $links as $link )
          echo $link;
     echo '</h2>'
}

We'll use this function shortly, as part of the Form markup.

Settings Page Form

First, we wrap our HTML in a container DIV, in order to apply the default WordPress Admin page style (everything that follows will be added inside these tags):

<div class="wrap">
<-- Markup goes here -->
</div>

Then, we add our tabs (using the helper function we defined previously):

Settings Page Tabs

Settings Page Tabs

<?php oenology_admin_options_page_tabs(); ?>

Then, we add the "updated" admin notice:

Settings Updated Notice

Settings Updated Notice

<?php if ( isset( $_GET['settings-updated'] ) ) {
    echo "<div class='updated'><p>Theme settings updated successfully.</p></div>";
} ?>

Then, we add the opening and closing FORM tags (everything that follows will be added inside these tags):

<form action="options.php" method="post">
<-- Markup goes here -->
</form>

Then, we call the functions that will build out the form:

<?php
settings_fields('theme_oenology_options');
do_settings_sections('oenology');
?>

Then, we add the 'Update Settings" submit button (which auto-magically updates itself according to the current tab):

<?php $tab = ( isset( $_GET['tab'] ) ? $_GET['tab'] : 'general' ); ?>
<input name="theme_oenology_options[submit-<?php echo $tab; ?>]" type="submit" class="button-primary" value="<?php esc_attr_e('Save Settings', 'oenology'); ?>" />

As a bonus, we'll also add a "Reset Defaults" submit button:

<input name="theme_oenology_options[reset-<?php echo $tab; ?>]" type="submit" class="button-secondary" value="<?php esc_attr_e('Reset Defaults', 'oenology'); ?>" />

And here is the form markup, in all its glory:

// Admin settings page markup
function oenology_admin_options_page() { ?>
     <div class="wrap">
          <?php oenology_admin_options_page_tabs(); ?>
          <?php if ( isset( $_GET['settings-updated'] ) ) {
               echo "<div class='updated'><p>Theme settings updated successfully.</p></div>";
          } ?>
     <form action="options.php" method="post">
     <?php
     settings_fields('theme_oenology_options');
     do_settings_sections('oenology');
     ?>
     <?php $tab = ( isset( $_GET['tab'] ) ? $_GET['tab'] : 'general' ); ?>
     <input name="theme_oenology_options[submit-<?php echo $tab; ?>]" type="submit" class="button-primary" value="<?php esc_attr_e('Save Settings', 'oenology'); ?>" />
     <input name="theme_oenology_options[reset-<?php echo $tab; ?>]" type="submit" class="button-secondary" value="<?php esc_attr_e('Reset Defaults', 'oenology'); ?>" />
     </form>
     </div>
<?php }

Surprised? Don't be. All the heavy lifting is handled by the two functions settings_fields() and do_settings_sections().

Function: settings_fields()

Codex: settings_fields()

The settings_fields() function adds nonces, the hidden action input, and the hidden page_options input to the form. Basically, it black-boxes everything that is needed to make the form work, and to make it secure.

The settings_fields( $option_group ) function accepts one argument:

  • $option_group: name of the option group. Must be the same as the $option_group defined in register_setting(), which we will address momentarily. This is the "flag" that ties everything together outside of the database (i.e. within the Settings page).
Function: do_settings_sections()

Codex: do_settings_sections()

The do_settings_sections() function adds the actual form sections and form fields to the form. Form sections are defined using add_settings_section(), and form fields are defined using add_settings_field(), both, of which we will address momentarily.

The do_settings_sections( $page ) function accepts one argument:

  • $page: name of the Settings page for which to output form sections and fields. Must be the same as the $page argument passed to add_settings_section(), which we will address momentarily. This argument correlates registered form sections to the form on which they are output.

Now that we have our Settings page markup, let's move on to defining our form sections and form fields.

Register Settings and Define Form Sections/Fields

Here, we use the Settings API to register our settings, and to define our Settings page form sections and form fields.

Register Settings

The first step is simple; we register our settings:

register_setting( 'theme_oenology_options', 'theme_oenology_options', 'oenology_options_validate' );
Function: register_setting()

Codex: register_setting()

The register_setting() function associates an option group (the $option_group argument passed to settings_fields() and do_settings_sections(), above) with a database entry and a data-validation callback function.

The register_setting( $option_group, $option_name, $sanitize_callback ) function accepts three arguments:

  • $option_group: the name of the option group. Must be the same as the $option_group defined in settings_fields(). This is the "flag" that ties everything together outside of the database (i.e. within the Settings page)
  • $option_name: the name of the option as stored in the database.
  • $sanitize_callback: the name of the function that holds the data validation and whitelisting

Since we have only one database entry, which is an array that holds all of our options, we only need to call register_setting() once. If we instead had multiple database entries, we would have to call register_setting() once for each database entry.

Note: this function call needs to be hooked into admin_init.

With this simple step out of the way, we now move on to the meat of our Settings API implementation: building out the form sections and form fields.

Define Form Sections and Fields

Separating Settings Per Tab

Our Settings page has multiple tabs, but we only defined one form. We only have one form, and therefore call settings_fields() and do_settings_sections() only once within that form. If you recall, both of these settings have arguments that tie both the database entry (via register_setting()) and the settings sections (via add_settings_section()) to the form.

We want to ensure that the form only outputs/handles the settings applicable to each tab. There are at least two ways to accomplish this separation:

  1. Assign each tab's related options to a separate $option_group and $page, and add a current-tab conditional around the settings_fields() and do_settings_sections() calls.
  2. Assign all options, regardless of tab, to the same $option_group and $page, and add a current-tab conditional around the functions that register the form sections and form fields.

Because it is what I thought of first, I went with the latter option; however, the former option might make a bit more semantic sense (assuming it works; I've not tried it).

Here's how I handled it:

global $pagenow;
if ( 'themes.php' == $pagenow && isset( $_GET['page'] ) && 'oenology-settings' == $_GET['page'] ) :
     if ( isset ( $_GET['tab'] ) ) :
          $tab = $_GET['tab'];
     else:
          $tab = 'general';
     endif;
     switch ( $tab ) :
          case 'general' :
               require( get_template_directory() . '/functions/options-register-general.php' );
               break;
          case 'varietals' :
               require( get_template_directory() . '/functions/options-register-varietals.php' );
               break;
     endswitch;
endif;

Essentially, a separate file is included, depending on which tab is displayed. Each file contains the code that registers the form sections and form fields related to each tab.

The $pagenow global is basically the URL of the current page. When we hooked our Settings page into the admin menu, the add_theme_page() function appends a parameter "page" to the URL, in the form of "page=oenology-options". Our custom function that handles tabs also adds a parameter, "tab", to the URL, in the form of "tab=general". So, if we are on the General tab of our Settings page, the URL would look something like:

http://www.domain.tld/wp-admin/themes.php?page=oenology-settings?tab=general

We take advantage of these URL parameters, via $pagenow, to build our conditional.

Defining Form Sections

Form sections are added via the add_settings_section() function.

Function: add_settings_section()

Codex: add_settings_section()

As its name implies, the add_settings_section() function defines a form section. Sections are simply groupings of (presumably similar/related) form fields. A settings form may have any number of sections. All form fields must be added to form sections.

The add_settings_section( $section, $title, $callback, $page ) function accepts four arguments:

  • $section: the name/id of the section. This argument correlates to the $section argument that is passed to each call to add_settings_field(), and is also passed as the CSS ID selector of the section's containing DIV.
  • $title: the title of the section, as output on the form.
  • $callback: function that contains markup/text to be output with the section, before associated settings fields are output (e.g. to provide explanatory text/description)
  • $page: the name of the Settings page for which to output the section. Must be the same as the $page argument passed to do_settings_sections(), which we addressed previously

We simply add one call to add_settings_section() for each form section.

General Tab

For the General tab, we will define two sections: one for Header options and one for Footer options:

// Add a form section for the Header settings
add_settings_section('oenology_settings_general_header', 'Header Options', 'oenology_settings_general_header_section_text', 'oenology');

// Add a form section for the Footer settings
add_settings_section('oenology_settings_general_footer', 'Footer Options', 'oenology_settings_general_footer_section_text', 'oenology');
Varietals Tab

For our Varietals tab, we will define only one section:

// Add a form section for the Varietal theme settings
add_settings_section('oenology_settings_varietal', 'Oenology Theme Varietals', 'oenology_settings_varietal_section_text', 'oenology');

Adding Form Fields to Sections

Form sections are added via the add_settings_field() function.

Function: add_settings_field()

Codex: add_settings_field()

As its name implies, the add_settings_field() function defines a form field.

The add_settings_field( $field, $title, $callback, $page, $section ) function accepts five arguments:

  • $field: the name/id of the field; also passed as the CSS ID selector of the field's containing DIV.
  • $title: the title of the field, as output on the form.
  • $callback: function that contains markup/text to be output
  • $page: the name of the Settings page for which to output the section. Must be the same as the $pageargument passed to do_settings_sections(), which we addressed previously
  • $section: the name of the form section to which the field is added. This argument correlates to the $section argument that is passed to each call to add_settings_section()
General Tab

For our General tab, we will define three options: two of which will belong to the Header options section, and one of which will belong to the Footer options section:

// Add Header Navigation Menu Position setting to the Header section
add_settings_field('oenology_setting_header_nav_menu_position', 'Header Nav Menu Position', 'oenology_setting_header_nav_menu_position', 'oenology', 'oenology_settings_general_header');

// Add Header Navigation Menu Depth setting to the Header section
add_settings_field('oenology_setting_header_nav_menu_depth', 'Header Nav Menu Depth', 'oenology_setting_header_nav_menu_depth', 'oenology', 'oenology_settings_general_header');

// Add Footer Credit Link setting to the Footer section
add_settings_field('oenology_setting_display_footer_credit', 'Footer Credit', 'oenology_setting_display_footer_credit', 'oenology', 'oenology_settings_general_footer');
Varietals Tab

For our Varietals tab, we will define one option, which will belong to our only defined section:

// Add Varietal setting to the Varietal section
add_settings_field('oenology_setting_varietal', 'Available Varietals', 'oenology_setting_varietal', 'oenology', 'oenology_settings_varietal');

And now that we have defined all of our form sections and fields, we move on to defining the callback functions that (finally!) contain all of the markup that gets output on the Settings page.

Form Section Callback Functions

The form section callback functions contain the markup/output that displays in the section, before the form fields are output.

General Tab

Here we define the markup for our two sections, Header options and Footer options:

General Tab Form Sections and Form Fields

General Tab Form Sections and Form Fields

// Header Settings Section
function oenology_settings_general_header_section_text() { ?>
     <p><?php _e( 'Manage Header options for the Oenology Theme. Refer to the contextual help screen for descriptions and help regarding each theme option.', 'oenology' ); ?></p>
<?php }

// Footer Settings Section
function oenology_settings_general_footer_section_text() { ?>
     <p><?php _e( 'Manage Footer options for the Oenology Theme. Refer to the contextual help screen for descriptions and help regarding each theme option.', 'oenology' ); ?></p>
<?php }
Varietals Tab

Here we define the markup for our single section:

Varietals Tab Form Section and Form Field

Varietals Tab Form Section and Form Field

// Varietal Settings Section
function oenology_settings_varietal_section_text() {
     $oenology_options = get_option( 'theme_oenology_options' );
     $oenology_varietals = oenology_get_valid_varietals();
     $imgstyle = 'float:left;margin-right:20px;margin-bottom:20px;border: 1px solid #bbb;-moz-box-shadow: 2px 2px 2px #777;-webkit-box-shadow: 2px 2px 2px #777;box-shadow: 2px 2px 2px #777;';
     foreach ( $oenology_varietals as $varietal ) {
          if ( $varietal['slug'] == $oenology_options['varietal'] ) {
               $oenology_current_varietal = $varietal;
          }
     } ?>
     <p><?php _e( '"Varietal" refers to wine made from exclusively or predominantly one variety of grape. Each varietal has unique flavor and aromatic characteristics. Refer to the contextual help screen for descriptions and help regarding each theme option.', 'oenology' ); ?></p>
     <img style="<?php echo $imgstyle; ?>" src="<?php echo get_template_directory_uri() . '/varietals/' . $oenology_options['varietal'] . '.png'; ?>" width="150px" height="110px" alt="<?php echo $oenology_options['varietal']; ?>" />
     <h4>Current Varietal</h4>
     <dl>
          <dt><strong><?php echo $oenology_current_varietal['name']; ?></strong></dt>
          <dd><?php echo $oenology_current_varietal['description']; ?></dd>
     </dl>
<?php }

This callback function is a bit more involved, as I am displaying the screenshot, title, and description of the currently selected Varietal, rather than merely outputting a text description as with the General tab sections.

Form Field Callback Functions

The form field callback functions contain the markup of the form fields themselves.

General Tab

Here we define the markup for our three General tab form fields:

// Navigation Menu Position Setting
function oenology_setting_header_nav_menu_position() {
     $oenology_options = get_option( 'theme_oenology_options' ); ?>
     <select name="theme_oenology_options[header_nav_menu_position]"<
          <option <?php selected( 'above' == $oenology_options['header_nav_menu_position'] ); ?> value="above">Above</option>
          <option <?php selected( 'below' == $oenology_options['header_nav_menu_position'] ); ?> value="below">Below</option>
     </select>
     <span class="description">Display header navigation menu above or below the site title/description?</span>
<?php }

// Navigation Menu Position Depth
function oenology_setting_header_nav_menu_depth() {
     $oenology_options = get_option( 'theme_oenology_options' ); ?>
     <select name="theme_oenology_options[header_nav_menu_depth]">
          <option <?php selected( 1 == $oenology_options['header_nav_menu_depth'] ); ?> value="1">One</option>
          <option <?php selected( 2 == $oenology_options['header_nav_menu_depth'] ); ?> value="2">Two</option>
          <option <?php selected( 3 == $oenology_options['header_nav_menu_depth'] ); ?> value="3">Three</option>
     </select>
     <span class="description">How many levels of Page hierarchy should the Header Navigation Menu display?</span>
<?php }

// Display Footer Credit Setting
function oenology_setting_display_footer_credit() {
     $oenology_options = get_option( 'theme_oenology_options' ); ?>
     <select name="theme_oenology_options[display_footer_credit]">
          <option <?php selected( false == $oenology_options['display_footer_credit'] ); ?> value="false">Do Not Display</option>
          <option <?php selected( true == $oenology_options['display_footer_credit'] ); ?> value="true">Display</option>
     </select>
     <span class="description">Display a credit link in the footer? This option is disabled by default, and you are under no obligation whatsoever to enable it</span>
<?php }
Varietals Tab

Here we define the markup for our single Varietals tab form field:

// Varietal Setting
function oenology_setting_varietal() {
     $oenology_options = get_option( 'theme_oenology_options' );
     $oenology_varietals = oenology_get_valid_varietals();
     $dlstylebase = 'float:left;padding:5px;text-align:center;max-width:160px;';
     $dlstylecurrent = 'border: 1px solid #999;-moz-box-shadow: 2px 2px 2px #777;-webkit-box-shadow: 2px 2px 2px #777;box-shadow: 2px 2px 2px #777;';
     foreach ( $oenology_varietals as $varietal ) {
          $currentvarietal = ( $varietal['slug'] == $oenology_options['varietal'] ? true : false );
          $dlstyle = ( $currentvarietal ? $dlstylebase . $dlstylecurrent : $dlstylebase ); ?>
          <dl style="<?php echo $dlstyle; ?>">
               <dt><strong><?php echo $varietal['name']; ?></strong></dt>
               <dd><img style="border: 1px solid #bbb;" src="<?php echo get_template_directory_uri() . '/varietals/' . $varietal['slug'] . '.png'; ?>" width="150px" height="110px" alt="<?php echo $varietal['name']; ?>" /></dd>
               <dd><input type="radio" name="theme_oenology_options[varietal]" <?php checked( $currentvarietal ); ?> value="<?php echo $varietal['slug']; ?>" /></dd>
               <dd><small><?php echo $varietal['description']; ?></small></dd>
          </dl>
<?php }

And that's it! Now our form properly outputs all necessary code to display and update our Theme options. However, before moving on, I briefly want to address how to enqueue custom CSS for our Settings page.

Enqueueing Admin CSS

In my above examples, I add some inline CSS. With only a handful of options, that's not such a big deal. However, it's not ideal for larger and more complex Settings pages. If you need to add custom CSS for such pages, you will want to use a stylesheet, which you will enqueue using wp_enqueue_style(), and then hook into the admin interface.

Now, you'll want your stylesheet to play nicely with the rest of the admin pages - ideally, you don't want to enqueue your stylesheet unless your Settings page itself is being displayed. Fortunately, WordPress includes a very powerful hook for just such a circumstance: admin_print_styles-{hook}, where {hook} is Theme/Plugin-specific. Nifty, eh?

First the code, then the explanation:

function oenology_enqueue_admin_style() {
     // define admin stylesheet
     $admin_handle = 'oenology_admin_stylesheet';
     $admin_stylesheet = get_template_directory_uri() . '/functions/oenology-admin.css';

     wp_enqueue_style( $admin_handle, $admin_stylesheet );
}
// Enqueue Admin Stylesheet at admin_print_styles()
add_action('admin_print_styles-appearance_page_oenology-settings', 'oenology_enqueue_admin_style', 11 );

Let's briefly take a closer look at how this is put together.

Function: wp_enqueue_style()

Codex: wp_enqueue_style()

The wp_enqueue_style() function is used to add a defined stylesheet to a WordPress-generated page. This function is used in conjunction with a hook in order to enqueue the stylesheet at the appropriate location.

The wp_enqueue_style( $handle, $src, $deps, $ver, $media ) function accepts several arguments, but for now we concern ourselves only with the first two:

  • $handle: the handle, or name, of the stylesheet to be enqueued
  • $src: the filepath to the stylesheet
Hook: admin_print_styles-{hook}

The admin_print_styles-{hook} hook is used to hook into a Plugin- or Theme-specific admin page. It takes the form $page_type . '_page_' . $page_slug:

$page_type: if using add_theme_page(), this is 'appearance'

$page_slug: this is equivalent to the page slug passed to the add_theme_page() function call, which in this case is 'oenology-settings'. (Note: this will be the same value as found in the URL parameter 'page=$page_slug'.)

So, putting it all together for our Theme, the hook name becomes:

admin_print_styles-appearance_page_oenology-settings

So, when we hook wp_enqueue_style() into this hook, our custom stylesheet will not load unless our Theme settings page is being displayed. No need for any conditional checks, or $pagenow, or anything else. Nice!

Now, we turn our attention to the final piece of the Settings API puzzle: validating user input.

Validate User-Input Form Data

Now we come to perhaps one of the most important Settings API functions: validation and whitelisting of user-input form data. We are going to define the callback function passed as the final argument to register_setting() earlier, and in which we will ensure that all user-input form data are of the correct type, correct bounds, and not malicious.

The validation function intercepts the user-input form data, contained in the $input variable, acts on those data, and then sends $input on its way to be updated to the database. We only want to return valid data to be added/updated to the database, so we do the following with $input:

  1. First define our "valid" data as the current value of our database option
  2. Then we update each appropriate option only if it meets our validation criteria.
  3. Then we pass our updated, "valid" input back to WordPress

Anything else that gets passed in via $input, but isn't part of our pre-defined "valid" input, is simply not processed (this is what is meant by "white-listing" our options).

First, we define the function:

function oenology_options_validate( $input ) {
	// validation code goes here
}

Then, we add the existing settings to an array, for later use within the function:

$oenology_options = get_option( 'theme_oenology_options' );
$valid_input = $oenology_options;

If you recall, our Settings page has two tabs, and each tab has two submit buttons ("Update Settings" and "Reset Defaults"). Our function will need to accommodate these four cases. So next, we determine which button the user submitted:

$submit_general = ( ! empty( $input['submit-general']) ? true : false );
$reset_general = ( ! empty($input['reset-general']) ? true : false );
$submit_varietals = ( ! empty($input['submit-varietals']) ? true : false );
$reset_varietals = ( ! empty($input['reset-varietals']) ? true : false );

Then, we perform our data validation for each use case. For updating settings, we will validate/sanitize the user input; for resetting defaults, we will simply reapply the default values for each setting.

General Tab Update Options

$valid_input['header_nav_menu_position'] = ( 'below' == $input['header_nav_menu_position'] ? 'below' : 'above' );
$valid_input['header_nav_menu_depth'] = ( ( 1 || 2 || 3 ) == $input['header_nav_menu_depth'] ? $input['header_nav_menu_depth'] : $valid_input['header_nav_menu_depth'] );
$valid_input['display_footer_credit'] = ( 'true' == $input['display_footer_credit'] ? true : false );

General Tab Reset Defaults

$oenology_default_options = oenology_get_default_options();

$valid_input['header_nav_menu_position'] = $oenology_default_options['header_nav_menu_position'];
$valid_input['header_nav_menu_depth'] = $oenology_default_options['header_nav_menu_depth'];
$valid_input['display_footer_credit'] = $oenology_default_options['display_footer_credit'];

Varietals Tab Update Options

$valid_varietals = oenology_get_valid_varietals();

$valid_input['varietal'] = ( array_key_exists( $input['varietal'], $valid_varietals ) ? $input['varietal'] : $valid_input['varietal'] );

Varietals Tab Reset Defaults

$oenology_default_options = oenology_get_default_options();

$valid_input['varietal'] = $oenology_default_options['varietal'];

And finally, we return $input, so that the Settings API can update the option.

return $valid_input;

Here is our validation function, all together:

function oenology_options_validate( $input ) {
     $oenology_options = get_option( 'theme_oenology_options' );
     $valid_input = $oenology_options;

     // Determine which form action was submitted
     $submit_general = ( ! empty( $input['submit-general']) ? true : false );
     $reset_general = ( ! empty($input['reset-general']) ? true : false );
     $submit_varietals = ( ! empty($input['submit-varietals']) ? true : false );
     $reset_varietals = ( ! empty($input['reset-varietals']) ? true : false );

     if ( $submit_general ) { // if General Settings Submit
          $valid_input['header_nav_menu_position'] = ( 'below' == $input['header_nav_menu_position'] ? 'below' : 'above' );
          $valid_input['header_nav_menu_depth'] = ( ( 1 || 2 || 3 ) == $input['header_nav_menu_depth'] ? $input['header_nav_menu_depth'] : $valid_input['header_nav_menu_depth'] );
          $valid_input['display_footer_credit'] = ( 'true' == $input['display_footer_credit'] ? true : false );

     } elseif ( $reset_general ) { // if General Settings Reset Defaults
          $oenology_default_options = oenology_get_default_options();
          $valid_input['header_nav_menu_position'] = $oenology_default_options['header_nav_menu_position'];
          $valid_input['header_nav_menu_depth'] = $oenology_default_options['header_nav_menu_depth'];
          $valid_input['display_footer_credit'] = $oenology_default_options['display_footer_credit'];

     } elseif ( $submit_varietals ) { // if Varietals Settings Submit
          $valid_varietals = oenology_get_valid_varietals();
          $valid_input['varietal'] = ( array_key_exists( $input['varietal'], $valid_varietals ) ? $input['varietal'] : $valid_input['varietal'] );

     } elseif ( $reset_varietals ) { // if Varietals Settings Reset Defaults
          $oenology_default_options = oenology_get_default_options();
          $valid_input['varietal'] = $oenology_default_options['varietal'];
     }
     return $valid_input;
}

At this point, we have fully implemented the Settings API. Whew!

Next, we turn our attention to adding contextual help - with tab-specific help content - to our Settings page.

Contextual Help

General Tab Contextual Help

General Tab Contextual Help

Varietals Tab Contextual Help

Varietals Tab Contextual Help

Configurable contextual help for WordPress admin pages is an awesome - if under-utilized - feature. Theme Settings pages represent an ideal use-case for adding contextual help, and doing so is incredibly easy, by passing our Theme-specific hook to the add_contextual_help() function, which we hook into admin_init:

function oenology_contextual_help() {

     $oenology_contextual_help_text = oenology_get_contextual_help_text();
     add_contextual_help( 'appearance_page_oenology-settings', $oenology_contextual_help_text  );
}
// Add contextual help to Admin Options page
add_action('admin_init', 'oenology_contextual_help', 10, 3);
Function: add_contextual_help()

Codex: add_contextual_help()

The add_contextual_help() function is used to add content to the contextual help tab for specific admin pages.

The add_contextual_help( $hook, $text ) function accepts two arguments:

  • $hook: the page to which to add the text. For our purposes, this is the hook defined by the add_theme_page() function call, and is the same as the {hook} defined previously for admin_print_styles-{hook}:
    appearance_page_oenology-settings
  • $text: the text (HTML markup) to add to the contextual help tab for the specified admin page.

As you can see, we define our contextual help text in a separate function. So next, we need to define that function:

function oenology_get_contextual_help_text() {

     $tabtext = '';
     require( get_template_directory() . '/functions/options-help.php' );
     return $tabtext;
}

(I've split the actual content, including the per-tab conditions, into a separate file for ease of management. Doing so is completely unnecessary. YMMV)

Inside this included file, we first determine which tab is being displayed:

if ( isset ( $_GET['tab'] ) ) {
     $tab = $_GET['tab'];
} else {
     $tab = 'general';
}

Then, we process only the text specific to the appropriate tab:

switch ( $tab ) {
     case 'general' :
          $tabtext = oenology_get_contextual_help_options_general();
          break;
     case 'varietals' :
          $tabtext = oenology_get_contextual_help_options_varietals();
          break;
}

Then, we define the actual text:

function oenology_get_contextual_help_options_general() {
     $tabtext = '';
     $tabtext .= <<<EOT
<h2>Header Options</h2>
<h3>Header Nav Menu Position</h3>
<p>The default location of the header navigation menu is above the site title/description. Use this setting to
display the header navigation menu below the site title/description.</p>
<h3>Header Nav Menu Depth</h3>
<p>By default, the Header Nav Menu only displays top-level Pages. Child Pages are displayed in the Sidebar Nav
Menu when the Top-Level Page is displayed. To change this setting:</p>
<ol>
     <li><strong>One</strong> (default) displays only the top-level Pages in the Header Nav Menu</li>
     <li><strong>Two</strong> displays the top-level Pages in the Header Nav Menu, and displays second-level
Pages in a dropdown menu when the top-level Page is hovered.</li>
     <li><strong>Three</strong> displays the top-level Pages in the Header Nav Menu, displays second-level
Pages in a dropdown menu when the top-level Page is hovered, and displays third-level Pages in a dropdown menu
when the second-level Page is hovered.</li>
</ol>
<h2>Footer Options</h2>
<h3>Footer Credit</h3>
<p>This setting controls the display of a footer credit link. By default, no footer credit link is displayed. You
are under no obligation to display a credit link in the footer or anywhere else.</p>
EOT;
     return $tabtext;
}
//
function oenology_get_contextual_help_options_varietals() {
     $tabtext = '';
     $tabtext .= <<<EOT
<h2>Varietals</h2>
<p><em>Varietals</em> are the <em>skins</em>, or styles, applied to Oenology.</p>
EOT;
     $oenology_varietals = oenology_get_valid_varietals();
     foreach ( $oenology_varietals as $varietal ) {
          $tabtext .= <<<EOT
<dl>
     <dt><strong>{$varietal['name']}</strong></dt>
     <dd>{$varietal['description']}</dd>
</dl>
EOT;
     }
     return $tabtext;
}

And that's it! Now our Setting page has tab-specific contextual help.

Finally, we come to the end: incorporating our shiny new Settings into the Theme.

Incorporate Settings Into Theme

And now, just a quick primer on incorporating our Settings into the Theme. I will touch on two use-cases: calling a Setting in a Theme template file, and enqueueing a stylesheet based on a Theme setting.

Template Files

This part is easy: we just dump our options array into a variable, and then call whichever option we need:

$oenology_options = get_option( 'theme_oenology_options' );

So for example, we can make use of the option to display the footer credit link:

if $oenology_options['display_footer_credit'] {
     // do something
}

Easy, right?

Let's move on to something slightly more advanced: enqueueing CSS.

Enqueueing CSS

This process won't be much different from enqueueing our admin stylesheet, except that we'll hook the stylesheet into a different hook. However, we're going to use a Theme setting to determine which stylesheet to enqueue.

Thanks to some forethought, each of the Theme's varietals - currently "Cuvee", "Syrah", and "Seyval Blanc" - has a corresponding stylesheet (named "cuvee.css", "syrah.css", and "seyval-blanc.css", respectively). These names also correspond to the "varietal" option value for each varietal (again, "cuvee", "syrah", and "seyval-blanc", respectively). This symmetry facilitates our use of wp_enqueue_style() to enqueue the appropriate stylesheet:

function oenology_enqueue_varietal_style() {
     // define varietal stylesheet
     global $oenology_options;
     $oenology_options = get_option( 'theme_oenology_options' );

     $varietal_handle = 'oenology_' . $oenology_options['varietal'] . '_stylesheet';
     $varietal_stylesheet = get_template_directory_uri() . '/varietals/' . $oenology_options['varietal'] . '.css';

     wp_enqueue_style( $varietal_handle, $varietal_stylesheet );
}
// Enqueue Varietal Stylesheet at wp_print_styles()
add_action('wp_print_styles', 'oenology_enqueue_varietal_style', 11 );

This time, we hook wp_enqueue_style() into the wp_print_styles hook.

Hook: wp_print_styles

The wp_print_styles hook is used to hook into the front-end stylesheet print queue.

And once again: that's it! Now, the Theme is enqueueing the appropriate stylesheet based on which varietal is selected.

Reference

Correlating Function Arguments

That was certainly a lot of information all at one time, with a lot of interconnected threads of which to attempt to keep track. So, I want to highlight how the various functions and arguments tie together.

Database Entry $option_name ("theme_oenology_options")

update_option() function

update_option( 'theme_oenology_options', $oenology_options )

Form Submit Buttons

<input name="theme_oenology_options[submit-<?php echo $tab; ?>]" type="submit" class="button-primary" value="<?php esc_attr_e('Save Settings', 'oenology'); ?>" />

Form Fields

<select name="theme_oenology_options[header_nav_menu_position]">
     <option <?php selected( 'above' == $oenology_options['header_nav_menu_position'] ); ?> value="above">Above</option>
     <option <?php selected( 'below' == $oenology_options['header_nav_menu_position'] ); ?> value="below">Below</option>
</select>

register_setting() function

register_setting( 'theme_oenology_options', 'theme_oenology_options', 'oenology_options_validate' )

Option Group $option_group ("theme_oenology_options")

register_setting() function

register_setting( 'theme_oenology_options', 'theme_oenology_options', 'oenology_options_validate' )

settings_fields() function

settings_fields('theme_oenology_options')

Settings Page $menu_slug ("oenology-settings")

add_theme_page() function

add_theme_page('Oenology Options', 'Oenology Options', 'edit_theme_options', 'oenology-settings', 'oenology_admin_options_page');

admin_print_styles-{hook}

add_action('admin_print_styles-appearance_page_oenology-settings', 'oenology_enqueue_admin_style', 11 )

Settings Page URL

http://www.domain.tld/wp-admin/themes.php?page=oenology-settings?tab=general

Contextual Help Hook

add_contextual_help( 'appearance_page_oenology-settings', $oenology_contextual_help_text  )

Settings Page $page ('oenology')

do_settings_sections() function

do_settings_sections('oenology')

add_settings_section() function

add_settings_section('oenology_settings_general_header', 'Header Options', 'oenology_settings_general_header_section_text', 'oenology')

Settings Section ($section)

add_settings_section()

add_settings_section('oenology_settings_general_header', 'Header Options', 'oenology_settings_general_header_section_text', 'oenology')

add_settings_field()

add_settings_field('oenology_setting_header_nav_menu_position', 'Header Nav Menu Position', 'oenology_setting_header_nav_menu_position', 'oenology', 'oenology_settings_general_header')

Summary

This tutorial has stepped through the process for implementing all of the following:

  • Registering options in the database as a single options array
  • Initializing default options
  • Creating a single Theme Settings page (with tabs)
  • Defining settings page sections and fields using the Settings API
  • Validating and white-listing user-input form data
  • Enqueueing custom CSS for the Settings page
  • Adding Settings Page contextual help
  • Implementing settings in the Theme template files
  • Enqueueing front-end CSS

If you have any questions, or any suggestions for improving this tutorial, please leave them in the comments.

Feedback

Comments (Comments are closed)

47 Responses to “Incorporating the Settings API in WordPress Themes”
  1. chip_bennett says:

    Incorporating the Settings API in WordPress Themes – http://www.chipbennett.net/2011/02/17/in… #wordpress

  2. Sayontan says:

    This is an exceptionally detailed tutorial, Chip! Looks like you beat me to it – I had started writing one based on my experience.

    I have grappled with a lot of these concepts while rewriting the options for Suffusion. In my case the situation was more complex – I have 2 levels of tabs: a horizontal one similar to your General / Varietals, and a vertical one within each. So basically I have GUI settings and Back-End Settings, with the first having Header settings, footer settings, fonts etc, and the back-end having tabs for SEO, Analytics etc.

    Also, instead of using one form, I had to use one form for each tab. Otherwise with my number of options I was running into issues with PHP installations that had Suhosin enabled (that restricts the number of post variables). That pushed the complexity to a whole different label, and I had to keep track of post variables in other forms (otherwise the whole array gets rewritten to only have options from the current form). Anyway, I guess you can see what I am saying when I submit my next version for review.

    One small note – in page 3 you are retrieving “settings-updated”. That applies to WP 3.1 onwards. In WP 3.0 you get “updated” as the returned URL parameter.

  3. chip_bennett says:

    Incorporating the Settings API in WordPress Themes – http://www.chipbennett.net/2011/02/17/in… #wordpress – I don’t do this often, but: please RT

  4. chip_bennett says:

    Incorporating the Settings API in WordPress Themes – http://www.chipbennett.net/2011/02/17/in… #wordpress – I don’t do this often, but: please RT

  5. Hi Chip,

    Great (and comprehensive!) tutorial here! One thing I had to find out the hard way: if you use the Settings API, you need the ‘manage_options’ capability to update the options. Normally this works fine, as only administrators have those two capabilities, but if you give editors the ability to edit theme options, they’ll need ‘manage_options’ too. This is built into the settings api, since it’s assumed it’s going to be used for options pages.

    Cheers!
    -John

  6. Chip Bennett says:

    @John P. Bloch

    Are you 100% certain that you are unable to use the edit_theme_options capability with add_theme_page()? I can confirm that I have used it with no problems whatsoever, and edit_theme_options is the capability officially recommended by the WordPress Theme Review Team and far-more-expert-than-I Theme developers such as Justin Tadlock.

  7. jonnyjaniero says:

    huge thanks for this. shines a big beam of light on the whole process.

  8. Looks like a fantastic tutorial.
    I didn’t read it yet, since now is not a good time for me to do this and so I wanted to print out the whole tutorial which sadly didn’t work.
    Not even using the print button to print individual pages worked but cropped content on each article.
    So I have to come back another time or cut and paste each part individually.
    Nevertheless, I think it will be worth it an am very greatful for this tutorial.

  9. Chip Bennett says:

    Christian:

    Check the post pagination links at the bottom of the post. I added an “all” link, that will output the entire post on a single page – specifically so that this post could be printed. Let me know if it’s not working for you!

  10. Rilwis says:

    Hi,

    I’ve read your article, and follow most of it. But I have a problem with tabs. As you said, you haven’t tried 2 options for tabs that handle form fields which are in current tabs. Sadly, when I update the form, only fields in current tab are save, fields in other tabs are ignored.

    I’m thinking that WP don’t automatically recognize tabs and save them. We need to do it ourselves. Do you know how to make it easily?

  11. Chip Bennett says:

    @Rilwis:

    I’m sorry; I’m not exactly following your question.

    The method that I describe here separates, via PHP, the form fields on one tab from the form fields on other tabs. When viewing one tab, the form fields on other tabs are never called. They don’t currently exist on the page. So, it is true: WordPress doesn’t save any settings not on the current page, because at the current point in time, WordPress doesn’t load the fields that allow those other settings to be modified.

    To do something different, such as loading all the settings on a single page, and then separate them via tabs, you’d have to take an entirely different approach (such as using jQuery to show/hide groups of settings).

  12. Sayontan says:

    @Chip, @Rilwis,
    How I handled this is by invoking the hidden fields in the validation function itself. Basically if you have separate forms you tend to lose the settings of other forms unless you have them conveyed to the back-end. Since you are anyway constructing the forms based on information you have in the back-end, it is easy to pull up the options from other screens in the validation function without passing them at form submission.

    Moreover I prefer passing it through the validation function because that keeps your form lighter.

  13. Rilwis says:

    @Chip,
    Sorry if I didn’t say clearly. Sayontan said exactly what I meant.

    It seems that the validation function is a good solution here. I’ll try it.

  14. Tobias says:

    Thanks, very useful tutorial!

  15. newbiewpcoder says:

    how can i display a message based on which button has been clicked for example if i click reset i would like it to display a message saying “settings reset” also like Sayontan says i had to change “settings-updated” to “updated” within the form code as it didn’t show the message “settings saved” when i clicked save settings

  16. Chip Bennett says:

    @newbiewpcoder:

    I’ll get back to you on your first question.

    Regarding your second question, by some point this morning (hopefully), you’ll need to use settings-updated, rather than updated. ;)

  17. newbiewpcoder says:

    thanks for this tutorial i’m building an options page using this method and trying to move all my old options over to this but i cannot get multi-select checkboxes to save as i have some older code to grab all pages into an array created in wordpress like

    $pages_array = get_pages('hide_empty=0');

    $site_pages = array();

    foreach ($pages_array as $pagg) {
    $site_pages[$pagg->ID] = $pagg->post_title;
    $pages_ids[] = $pagg->ID;
    }

    function oenology_get_default_options() {
    $options = array(
    'header_nav_menu_position' => 'top',
    'header_nav_menu_depth' => 1,
    'display_footer_credit' => false,
    'varietal' => 'cuvee',
    'menu_pages' => $pages_ids

    );
    return $options;
    }

  18. @Chip

    (Sorry for the delayed response)
    ‘edit_theme_options’ for the theme page is fine. It’s the Settings API that requires the ‘manage_options’ capability. The settings API requires you to send form data to /wp-admin/options.php, which, on lines 30-31 has this code:

    if ( !current_user_can('manage_options') )
    wp_die(__('Cheatin’ uh?'));

    The theme options page that this code creates will work without a problem, and saving the content will work without a problem — most of the time.

    The case where it doesn’t work is when you have anybody who can edit theme options but cannot manage options.

    For example, I program clients’ sites with a non-standard ‘Admin Lite’ role which is an editor that can create, edit, and delete non-Administrator users, as well as manage theme options. Because of the way options.php handles capabilities, I cannot use the settings API to validate or save the data from my theme options page. These are very specific use cases, and it might help to know about this behavior for anybody looking up this tutorial in the future. Just trying to share some hard-won knowledge in case it’s useful. :)

    -John

  19. newbiewpcoder says:

    you can delete my last comment as i managed to solve it myself, i have created a function to return each option here’s the function

    function theme_get_option($name) {
    $options = get_option('theme_oenology_options');
    if (isset($options[$name])) {
    return $options[$name];
    } else {
    return false;
    }
    }

    example of usage

    of course you may not want to echo the option but you get the idea

  20. newbiewpcoder says:

    oops my code was cut off here it is

    echo theme_get_option('display_footer_credit');

Trackbacks

  1. The WordPress Setting API | Roberto Baca
  2. ????Settings API??????????Zespia
  3. Adding Theme Options To Bloggy | Poetic Coding
  4. The WordPress Theme Review Experiment – Take II » Aquoid Themes