How to create a WordPress Theme Options Page

Having an options page for your WordPress theme is a great way to increase the theme’s extensibility, usability and ease of operation. At first glance, implementing such a page may seem a hard task and not so long ago it was; however, now with the new WordPress Settings API introduced in version 2.7, creating an options page has become a simple task …

Themefuse Having an options page for your WordPress theme is a great way to increase the theme’s extensibility, usability and ease of operation. At first glance, implementing such a page may seem a hard task and not so long ago it was; however, now with the new WordPress Settings API introduced in version 2.7, creating an options page has become a task as simple as registering the required settings and then calling them where required.

The Settings API was created in an effort to streamline the  calling for options in WordPress themes and plugins and also, to provide an easy, more secure way to do it. Here, I will present a very simple way to implement this functionality, but that meets all guidelines from the WordPress documentation.

You see, the problem here is that this API exists for almost 2 years and people just tend to stick to what they know and do it   the old way. But that’s wrong for the following reasons:

  • It’s hard
  • It’s not secure
  • The code is a mess

While the Settings API offers:

  • Easyness
  • Hardened security
  • Compatibility with the WordPress Core

So down to business, here’s how we’ll create together a sample options page for your WordPress theme:

Let’s say we’ll make a page with five options: An option that changes the footer copyright text, an intro text to show on the front page, a drop down list to select a category to show as featured on the front page, some radio buttons to choose how we want to main layout to look like, and a checkbox whether to show the autor’s credit links.

At the end, we are going to bundle the entire code into a functioning child theme for Twenty Ten called “Settings API”.

We are going to prefix all the function and option names used here with a shortname of our theme, in this case “Settings API”, hence “sa_”. This is to avoid conflicting with plugins that may use similar names.

First, we’ll need to register the options we’ll use, and we’ll do this with the function register_setting, here’s how:

function sa_register_settings() {
	register_setting( 'sa_theme_options', 'sa_options', 'sa_validate_options' );

add_action( 'admin_init', 'sa_register_settings' );

The register_setting function needs to be called within a function hooked to the admin_init action or else it will report a call to undefined function error.

Note: We have registered a single setting, which we will use as an array containing all the settings we need for the theme. This is the recommended way to do it, but individual settings can be registered as well.

Now, here’s what all these parameters mean:

The first parameter is the is the so called options group. This can be set to any string, but all settings belonging to this group will need to be called within the same form, or the ones missing will be voided.

The second parameter is the actual setting name, this can also be given any name and it’s the name the setting will be called with.

The third parameter is the name of the function that sanitizes the submitted value, to make sure it’s actually within the declared values and to avoid vulnerabilities such as cross-site scripting also knows as XSS. This step is optional but highly recommended. We’ll get into this later.

So, that’s all that was necessary to declare these options as existent. At this point the options can be called in the theme with the function get_option:

$settings = get_option('sa_options');

Although at this point the function will not return any errors, it won’t return any value as well, because an option for footer_copyright has not been given yet and a default value has not been set. That’s why we need to seet some default values, to make the options usable, before the user touches the options form. We’ll do this within an array:

$sa_options = array(	'footer_copyright' => '© ' . date('Y') . get_bloginfo('name'),
	'intro_text' => '',
	'featured_cat' => 0,
	'layout_view' => 'fixed',
	'author_credits' => true

We have given the footer copyright notice a default “© 2011 Site Name”, left the intro text blank since maybe the owner doesn’t want to display anything, set no default featured category,  the layout to fixed and the author credits to show.

Giving these values in an array doesn’t actually make them default for these settings, just stores them for later use. The way we pass the default value to the option, is by calling it with the default value as the second parameter:

$settings = get_option( 'sa_options', $sa_options )

Now that we have the default values, we’ll also need to declare the available values, the ones that will show off in the options form, for the options that have multiple choices. Since we’re using a form to submit the options, the most convenient way to do this is by making each value into an array, that has both the input’s value and label. Here’s how:

For the featured category, we may want to store all the existing categories into an array:

$sa_categories[0] = array(
	'value' => 0,
	'label' => ''
); // Void first array key to disable feature
$sa_cats = get_categories();
foreach( $sa_cats as $sa_cat ) :
	$sa_categories[$sa_cat->cat_ID] = array(
		'value' => $sa_cat->cat_ID,
		'label' => $sa_cat->cat_name

An now we have all the categories’ IDs and names stored in the $categories array.

$sa_layouts = array(
	'fixed' => array(
		'value' => 'fixed',
		'label' => 'Fixed Layout'
	'fluid' => array(
		'value' => 'fluid',
		'label' => 'Fluid Layout'

And the desired layout options in the $layouts array.

Now we’re ready to create the theme options page. First we’re going to register it with the function add_theme_page:

function sa_theme_options() {
	add_theme_page( 'Theme Options', 'Theme Options', 'edit_theme_options', 'theme_options', 'theme_options_page' );

add_action( 'admin_menu', 'sa_theme_options' );

The function add_theme_page needs to be called within a function hooked to the admin_menu action or else it will report a call to undefined function error. This function will add a submenu item to the Appearance menu of the admin panel. This is the recommended location for theme options pages, and the one people are used to. Here’s what all the parameters mean:

The first parameter is the document title, the one on the browser’s title bar, for the options page.

The second parameter is the name of the menu item that will appear under Appearance.

The third parameter is the capability needed by the user to access the page. We have set this to ‘edit_theme_options’ since this is what we’re doing.

The fourth parameter is the options page slug, the one that will appear in the page’s URL.

And finally the last parameter is the call to the function that generates the page, and now we’re getting to that.

For the options page we are going to use the same layout as standard admin pages inside WordPress to preserve consistency:

function sa_theme_options_page() {
	global $sa_options, $sa_categories, $sa_layouts;

	if ( ! isset( $_REQUEST['updated'] ) )
	$_REQUEST['updated'] = false; // This checks whether the form has just been submitted. ?>


	<?php screen_icon(); echo "<h2>" . get_current_theme() . __( ' Theme Options' ) . "</h2>";
	// This shows the page's name and an icon if one has been provided ?>

	<?php if ( false !== $_REQUEST['updated'] ) : ?>
	<div><p><strong><?php _e( 'Options saved' ); ?></strong></p></div>
	<?php endif; // If the form has just been submitted, this shows the notification ?>

	<form method="post" action="options.php">

	<?php $settings = get_option( 'sa_options', $sa_options ); ?>

	<?php settings_fields( 'sa_theme_options' );
	/* This function outputs some hidden fields required by the form,
	including a nonce, a unique number used to ensure the form has been submitted from the admin page
	and not somewhere else, very important for security */ ?>

	<table><!-- Grab a hot cup of coffee, yes we're using tables! -->

	<tr valign="top"><th scope="row"><label for="footer_copyright">Footer Copyright Notice</label></th>
	<input id="footer_copyright" name="sa_options[footer_copyright]" type="text" value="<?php  esc_attr_e($settings['footer_copyright']); ?>" />

	<tr valign="top"><th scope="row"><label for="intro_text">Intro Text</label></th>
	<textarea id="intro_text" name="sa_options[intro_text]" rows="5" cols="30"><?php echo stripslashes($settings['intro_text']); ?></textarea>

	<tr valign="top"><th scope="row"><label for="featured_cat">Featured Category</label></th>
	<select id="featured_cat" name="sa_options[featured_cat]">
	foreach ( $categories as $category ) :
		$label = $category['label'];
		$selected = '';
		if ( $category['value'] == $settings['featured_cat'] )
			$selected = 'selected="selected"';
		echo '<option style="padding-right: 10px;" value="' . esc_attr( $category['value'] ) . '" ' . $selected . '>' . $label . '</option>';

	<tr valign="top"><th scope="row">Layout View</th>
	<?php foreach( $layouts as $layout ) : ?>
	<input type="radio" id="<?php echo $layout['value']; ?>" name="sa_options[layout_view]" value="<?php esc_attr_e( $layout['value'] ); ?>" <?php checked( $settings['layout_view'], $layout['value'] ); ?> />
	<label for="<?php echo $layout['value']; ?>"><?php echo $layout['label']; ?></label><br />
	<?php endforeach; ?>

	<tr valign="top"><th scope="row">Author Credits</th>
	<input type="checkbox" id="author_credits" name="sa_options[author_credits]" value="1" <?php checked( true, $settings['author_credits'] ); ?> />
	<label for="author_credits">Show Author Credits</label>


	<p><input type="submit" value="Save Options" /></p>




Here’s what we’ve done above:

  • We’re checking if the form has just been submitted and display a notification the options have been saved
  • We’re opening the form
  • We’re calling the function settings_fields (the passed parameter must be the same as the options group) to output some required hidden fields, including a nonce. A nonce (short for “number used once”) is a unique number passes along with the form to ensure is has actually been submitted from the options page. You can read more about Nonces at the Codex.
  • Then, we call each option and pass it on to the input fields. Notice we are not directly echoing the values but escaping them through functions like esc_attr, this is the recommended way.

Important Note: The name of the input field must be the same with the name of the option being submitted.

Now there’s one more thing we’ll do to make the options values more secure: we’re going to validate each input and make sure it’s value is within the allowed ranges. For this we declare a sanitation function, which is called automatically when the form is submitted, with the input value passed as an argument. Here it is:

function sa_validate_options( $input ) {
	global $sa_options, $sa_categories, $sa_layouts;

	$settings = get_option( 'sa_options', $sa_options );

	// We strip all tags from the text field, to avoid vulnerablilties like XSS
	$input['footer_copyright'] = wp_filter_nohtml_kses( $input['footer_copyright'] );

	// We strip all tags from the text field, to avoid vulnerablilties like XSS
	$input['intro_text'] = wp_filter_post_kses( $input['intro_text'] );

	// We select the previous value of the field, to restore it in case an invalid entry has been given
	$prev = $settings['featured_cat'];
	// We verify if the given value exists in the categories array
	if ( !array_key_exists( $input['featured_cat'], $categories ) )
	$input['featured_cat'] = $prev;

	// We select the previous value of the field, to restore it in case an invalid entry has been given
	$prev = $settings['layout_view'];
	// We verify if the given value exists in the layouts array
	if ( !array_key_exists( $input['layout_view'], $layouts ) )
	$input['layout_view'] = $prev;

	// If the checkbox has not been checked, we void it
	if ( ! isset( $input['author_credits'] ) )
	$input['author_credits'] = null;
	// We verify if the input is a boolean value
	$input['author_credits'] = ( $input['author_credits'] == 1 ? 1 : 0 );

	return $input;

Standard PHP functions can also be called as sanitation functions, as for example absint to verify if the value is an integer greater than 0. More information about data sanitation can be fount at the Codex.

Here we have it: a simple, secure theme options page ready to go!

Now let’s see some usage of the options. First, we’re going to store the options in a variable called $sa_settings:

global $sa_options;
$sa_settings = get_option( 'sa_options', $sa_options );

Here’s how the section of the footer displaying the copyright notification should look like:

<div id="footer">
<?php echo $sa_settings['footer_copyright']; ?>

This will simply output what has been given in the theme options, or the default.

Here’s how the intro text section should be called:

<?php if( $sa_settings['intro_text'] != '' ) : ?>
<div class="intro">
<?php echo $sa_settings['intro_text']; ?>
<?php endif; ?>

Displaying the featured category:

<?php if( $sa_settings['featured_cat'] ) : ?>
<div class="featured">
<?php query_posts('cat=' . $sa_settings['featured_cat'] ); ?>
<?php if(have_posts()) : while(have_posts()) : the_post(); ?>
// The Loop
<?php endwhile; endif; rewind_posts(); wp_reset_query(); ?>
<?php endif; ?>

Displaying the layout:

#wrapper {
	<?php if( $sa_settings['layout_view'] == 'fixed' ) : ?>
	width: 960px;
	<?php else: ?>
	padding: 0 20px;
	width: 100%;
	<?php endif; ?>

Showing credit links:

<?php if( $sa_settings['author_credits'] ) : ?>
<a href="">Created by Author</a>
<?php endif; ?>

These functions have been bundled in a nice Twenty Ten child theme called “Settings API”, you can download it below:

Download Source Code

Hope this helps in building better, more secure options pages.

49 thoughts on “How to create a WordPress Theme Options Page

    1. Have you used the provided source file or copy pasted code from the article?

      It may be that invalid characters have escaped, like quotation marks.

    1. Indeed, there were some syntax errors I omitted by mistake.

      I have bundled now the source code into a child theme for Twenty Ten, tested it and it’s working fine.

  1. Hi Daniel,
    Your included theme breaks WordPress! The front end site doesn’t work with your theme enabled.

    That’s because you have a bunch of code and variables in global scope. Specifically $cat interferes with the global one used by WordPress in query processing.

    You should wrap that code in an access function or at least prefix the variable names with your unique ‘sa_’ prefix.

    It’s also worth remembering that that code is executed *every single request* because your theme’s functions.php is executed every single request. You only need to run that code on the admin pages.

    First choice: wrap access functions around those arrays; second choice: wrap an is_admin() test around the code and prefix the names.

    Similarly, all your function names should use your prefix. ‘register_settings’ is a very obvious name that any other theme or plugin could use. They shouldn’t but if here is a name clash and your theme was activated second it will get the blame.

    Hope that helps,

  2. Great post! Thank you! This is exactly what I’ve been searching for.

    I’m just starting to work with the admin side of WordPress… do you have tips for creating custom Post Types and Custom Fields?

    I’ve worked with both of these, but from random info I’ve found via the web… Your techniques above seem to be more stable and I’m wondering if you would have a better way of creating these than what I’m currently using.


  3. I just wanted to extend a HUGE thank you for this post. I’ve gone through so many tutorials on adding a theme options page but none of them provided me with what I was looking for. Your approach kept things relatively simple and intuitive which has allowed me to customize this functionality effectively.

    Thanks again,
    Mark Wasyl

  4. how can i grab all pages created in wordpress into an array and display mutli-select checkboxes in the options page ?

    1. You can grab all pages with the get_pages() and you can create an array in the options page by passing blank keys in the name fields.

      ≤input type="checkbox" name="mytheme_options[pages][]" value="1" />

      This code isn’t completely functional but I hope you get the base idea.

  5. i could not manage to get it working i tried for hours as im trying to move over from the old way do you have any working code thanks

    1. I’ve just seen that code didn’t show correctly, I corrected it now.

      Well, what you basically need to do is to create a foreach loop and pass a name corresponding to the page to the array key. I don’t have any working code as I have never tried this.

  6. i managed to get the pages displayed as checkboxes in the options page however it doesn’t save my options any ideas

  7. Thank you this was great.
    The only problem I am having is that the featured_cat does not work.
    For testing I placed;
    in front of the select statement and it never shows anything.


  8. Thanks for the article.
    I tried customising the options page for my own options and gave a text input field in the form a name of “sa_options[sa_bannerimage]”. I assume the register_setting( ) function with “sa_options” as the 2nd parameter should therefore store the value in the array in the database entry. But it’s not working….

    Could you see my wp support article:

    1. How do you retrieve the option? The correct way is this:
      $options = get_option( ‘sa_options’ );
      $bannerimage = $options[‘sa_bannerimage’];

      Also, you need to hook the register_setting function to admin_init

      Did you add the settings_fields call?

  9. Daniel,

    thanks for your instructive example. In the downloaded code, I notice you’re checking early if it’s an admin page, like this:

    if ( is_admin() ) : // Load only if we are viewing an admin page

    but since you’re only adding an action to admin_init, like this:

    add_action( ‘admin_init’, ‘sa_register_settings’ );

    is the earlier check necessary? I’m just trying to understand as much as I can how theme options work.

    1. Although the functions are not called they are still parsed and compiled on each request. The reason the conditional is there is to keep normal requests as light as possible.

      1. I thought it was something like that, but wanted to make sure. Thanks for the info.

        Since theme-options.php needs to be included in functions.php (unless I’m mistaken), I put the conditional there, like so:

        if (is_admin()) // Load only if we are viewing an admin page

        this is working well in a childtheme I created. 🙂

  10. Hi Daniel

    Just want to say thank you.
    This article of yours is superb !

    I have one question though,
    do you think we should avoid using in the admin page ?

  11. I must say that you have a wonderful tutorial, which I have modified to include tabs and extra options. (very easy to follow)

    But I do have a few questions.
    How would I incorporate the following options:
    1. Image Upload
    2. a reset option (restore defaults)


  12. Thanks for this. It helped me update my personal theme framework to use the settings API (something I’d been meaning to do for a while, but never got around to). One thing I wanted to mention, you have a slight problem with your code. As it is, WP will never show the saved notice, because at some point the query variable names got changed. Instead of using $_REQUEST[‘updated’], you need to use $_REQUEST[‘saved’].

  13. Nice tutorial, thanks for posting it. I have a question about the options framework. Do you know if and how to use muliselect boxes with the framework? I’ve seen them referenced as combo-boxes too. It is a regular select with the multiple attribute set to “multiple”. It allows you to select multiple items if you have Ctrl held down.

    I have a fully functional plugin options page with a multi-select box. I am obviously able to select multiple items. When you update the options page (submit the form), it only saves the first item you selected.


    1. I have no knowledge of the options framework, sorry, But from what you’re saying this is probably achieved with JavaScript and probably a script isn’t declared properly.

      1. Sorry, I misspoke. I was playing with Devin’s Options Framework and mixed the two up. This is not related to the Options Framework.

        I am using the Settings API as your tutorial suggests.

        As an example, is there a way to turn your featured_cat select box into a multiple select?

          1. The select box does allow multiple (I’ve added that very attribute) but the data appears to be filtered out by WP’s options page prior to validation. I’ve tried all I know in the validation function to extract the array but the data just doesn’t appear to be there.

            I’ll keep looking. I appreciate the help though.

        1. I think you just need to make sure to name your form variable as an array
          e.g. name=”multiplechoices[]”
          Then the result will be delivered as an array in PHP.


          1. Hey Mike. Thanks for the tip. I’ve read that other places too. The problem is that with the Settings API you cannot leave the index blank. The name of the select needs to be something like “sa_options[pages]”. The API then passes the index “pages” to the validation function which allows me to sanitize and set the option in the DB. If you know another way to do this, please let me know.

          2. @sergio Following on from my earlier response and applying it to the example code, you need to name your select field like this:


            Note the second level square brackets.

            That then works! I’ve just tried it. You need to change the validate code, the form drawing code, and of course the front end code. But I’ve just had it all working.


  14. Hey Mike. You solved my problem. Thanks a ton for putting in the time to help with this.

    For others reading this, I needed to cast the input var in the validation function to an array (i.e., $valid_input[‘posts’] = (array) $input[‘posts’];). The form drawing code should be straightforward as get_option returns an array, which you can parse through to see what items are selected.

  15. I’m not a theme designer, but I saw a discussion recently which discussed how themes with a big “back end” – lots of customisation options – make the theme slow to load. Which as you know, is a bad thing!

    Woo Themes and Elegant Themes – two highly respected theme designers – were singled out for having many themes that ran like a dog. And I must say, I did try some themes from those companies and they slowed my page load times dramatically. I was sorry about that, because the themes were positively droolworthy.

    The thing is, there was a lot of talk about page load times in the blogosphere at one time, but it barely gets mentioned now,and I see a lot of bloggers who are clearly unaware of it – loading their blogs down with all kinds of gimmicks. I do like to have a custom background and header, and a slider is great, but I wish someone out there did all of that and also made their theme LIGHTWEIGHT AND FAST!

    1. The only reason when theme options can interfere with page load time is if they’re loaded on each and every request. The proper way to do it is to wrap all theme options functions in `if( is_admin() )` conditionals. Also, when retrieving the options from the database, a global should be set to store these settings to avoid doing the same query several times. Also avoided should be doing image processing on the fly and other processor intensive operations. If the above are done properly any theme could easily handle some dozens of options.

      1. Those are good tips Daniel. The is_admin conditional is key for this and should prevent the site from slowing down due to options code. My strategy was to make sure all options code was in functions and to add only two options hooks in the is_admin block. You can easily see that that site only runs that code if you’re in the admin section of your site.

        WP provides all sorts of ways to limit which pages load what code. You make a good point that a lot of themes and plugins don’t put much thought into this. Some are worse than others of course.

  16. Hi, I installed the child theme but on clicking save settings i get this:

    Warning: Cannot modify header information – headers already sent by (output started at /home/simplecs/public_html/wp/wp-content/themes/beta 2.0/functions.php:1) in /home/simplecs/public_html/wp/wp-includes/pluggable.php on line 866

    Any ideas why this would happen, as it seems to be a native wordpress file thats causing the issue.

    kind regards,


    P.S. very good tutorial 😛 got me this far 🙂

    1. You probably have white spaces at the beginning or end of functions.php. Make sure there are no spaces before the forst . In fact it’s better to omit the last ?> completely.

      1. Yep 🙂 found it, thanks very much, it wasn’t that there was whitespace but there was a style before it *looks down in shame*

  17. Hi and thanks for this tutorial. I could use some advise though. I created a theme options page with choice between left and right sidebar but i would like to add a option for a three collumn layout, a left and right sidebar. Could you give some advise on how to do this? thanks.

    1. I can see two possible ways, both requiring the registration of 2 sidebars. The first way is to to hide one of them when a one sidebar layout is selected. This would have the drawback of not showing some content which may be unexpected user behavior. The second way would be to stack the sidebars one over the other to form a single sidebar when one sidebar layout is selected. This is more difficult to implement but makes sure all content is visible at all times.

      1. I added a third sidebar to Twenty Ten a while back, did the same with Twenty Eleven. I found the best way was to stack them. Following Twenty Elevens CSS it was easy to make the second sidebar responsive liek the first one too.

        The only problem was in a child theme it’s a bit of work to reorder the widget areas in the admin section so the second sidebar was in second place and not first.

        Wish I had seen this tutorial before I created my theme, the options are much better than how I did it. Thanks for it.

  18. Hi, thanks for this tutorial. I adapted it to just make a list of theme options in a “select” dropdown. But it doesn’t seem to keep the selection; I get the “Options saved” message but nothing actually changed. Any ideas of what I might have changed that I shouldn’t have? Thanks in advance.

      1. Thanks, working now. I downloaded the code instead of getting it from this page, and then went over my code and tweaked a few things.

  19. It might be a fun idea to include filenames in the future. Now I found myself forced to download your code just to see what goes where. Anyway, thank you for sharing this information. I probably could have found it in the wordpress docs somewhere, but I don’t want to go through all that trouble as I don’t really care for wordpress and its horrible code. Either way, thank you for saving me time.

Leave a Reply

Your email address will not be published. Required fields are marked *