How to Make a WordPress Admin Options Page (Without Using the Settings API)

settings | WordPress admin menu settings

We’re going to cover creating a working WordPress options page, without using the Settings API.

I don’t like the WordPress Settings API. And every time we talk about writing about it, I start whining like the most annoying child you can imagine. I whine because the Settings API is the most official way to make admin options pages in WordPress, which is an important thing to do for both public and private plugins. But to put it kindly, I find the WordPress Settings API baffling and more than a little bit quirky.

But there are parts to making options pages that don’t really involve the Settings API. And in fact, you don’t ever need to use it at all. Today we’re going to describe how to create an admin options page, and how to save a site-wide option—but without using the Settings API.

We’ll talk first about how to make a page that just shows up in the WordPress admin area, and then we’ll go into more detail about how to save and load options safely on a page.

A High-Level Overview of a WordPress Options Page

In WordPress, an admin options page is a page that the site administrator accesses to change something about his or her site. Any page listed under “Tools” or “Settings,” for example, is an admin options page. And a large plugin like WordPress SEO by Yoast might register an entire admin submenu full of admin options pages:

Example of a WordPress options page

All admin menu pages have markup that describes to the user what the page allows him or her to change. And the heart of every admin menu page is one or more settings, outputted to the user by a web <form> element, that changes something about your website.

Basic Steps to Create an Admin Options Page

Creating an admin menu page with working settings has three main steps:

  1. Creating a page in the admin area.
  2. Writing the page contents.
  3. Within the page contents, creating a working form that will save the settings you wish to change.

Where We’re Dodging the Settings API

Step 3 is the one that usually requires the Settings API. However, you can accomplish the same goal by creating a simple, old-school-PHP-style Options Page in the WordPress backend, and avoid using the Settings API altogether.

And while the community, the Codex, and most of your peers will tell you you’re doing it wrong, this does work, and it cuts down on the incidental complexity (that is, the poor API design) that travels along with the Settings API.

So that’s a high-level introduction. We’ll be walking you through each step below.

1. Creating a Page in the Admin Area

Your first step is to make a page in the admin area of your site.

Your first step is to make a page in the administration area of your site, to contain your page content and <form>. To do this, you’ll use one of a list of similar functions which all have the same basic naming pattern: add_*_page(). With the right one of those guys, you can create a new administration page pretty much wherever you want.

The most common and easy of these is the add_menu_page() function. Each of these functions has a pretty long list of parameters, and unfortunately, these parameters aren’t in a very clear order. (This is the heart of my whole complaint about this area of WordPress code.) From the Codex:

add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $function, $icon_url, $position );

I’ll quickly explain each parameter:

  1. $page_title is the thing that WordPress might actually display on the page. (It turns out in most code, including ours, people end up discarding the value.)
  2. $menu_title is how your page will appear in the sidebar.
  3. $capability is the string representation of the WordPress user capability system that you’ll want someone who can interact with this page to have.
  4. $menu_slug is the url-safe representation that your page will use.
  5. $function is the callback for the function you want to render your actual page.
  6. $icon_url is the URL (or dashicon signifier) that you want your page to have.
  7. $position is an integer that determines where (height-wise) in the left sidebar your option should appear, 1 being the top, 99 being the bottom. (See the annotated guide below to get a sense of it in more detail.)

wordpress-add-menu-page-location-map

Only the first four parameters (to $menu_slug) are mandatory, but I’d include the fifth as well, because the behavior without it is kind of confusing.

Managing the Complexity and Illegibility of the add_*_page() functions

One of the big drawback to that fact that this function, and most others similar to it, takes a very large number of parameters is that you’ll forget what each does have how it works pretty quickly.

Because PHP doesn’t support named parameters, I’d recommend keeping your parameters very close and easy to refer to. What that means is that instead of doing this:

add_action('admin_menu', 'awesome_page_create');
function awesome_page_create() {
    add_menu_page( 'My Awesome Admin Page', 'Awesome Admin Page', 'edit_posts', 'awesome_page', 'my_awesome_page_display', '', 24);
}

You can do this:

add_action('admin_menu', 'awesome_page_create');
function awesome_page_create() {
    $page_title = 'My Awesome Admin Page';
    $menu_title = 'Awesome Admin Page';
    $capability = 'edit_posts';
    $menu_slug = 'awesome_page';
    $function = 'my_awesome_page_display';
    $icon_url = '';
    $position = 24;

    add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $function, $icon_url, $position );
}

One of my sources of frustration with these functions (and the Settings API as a whole) is that I’ve historically favored the first form over the second. It has the advantage of being slightly more concise; but when I come back later to try to look at or change the code, I’m always confused.

This interface has too many arguments for you to favor conciseness. The second example makes up for PHP’s lack of named parameters by naming by proxy, and it creates far more sustainable and legible code in the long term.

The Other add_*_page() Functions

As I said, all add_*_page() functions are similar. Rather than exhaustively list them and highlight how modestly different each of them is, I’ll point you to the Codex page that shows them all off: Administration Menus API. A very short summary is that if you don’t want to create a top-level menu (one as big and prominent as “Dashboard”, “Posts” or “Plugins”), you want to create a submenu. And there’s one main function you use for this, and a whole lot of shortcuts with slightly better naming.

Basically, if you want to create a new page that’s under the “Posts” sidebar menu, your options are these two:

$parent_slug = 'edit.php';
add_submenu_page( $parent_slug, $page_title, $menu_title, $capability, $menu_slug, $function );

// or

add_posts_page( $page_title, $menu_title, $capability, $menu_slug, $function );

As I’ve already mentioned my personal complaint about opaque and confusing long lists of parameters, you can probably guess my preference. The main reason that you’d use add_submenu_page() vs. something like add_posts_page(), to my mind, is when you’ve created both the parent and the children, so you’re unable to use the shortcut function because it doesn’t exist.

2. Using the Callback Function to Make Your Page

You’ll need to use a callback function to output content to your page.

The heart of making your administration page is using the $function parameter to actually output your page. Remember that one of the arguments we passed into add_menu_page() using our my_awesome_page_display() function above is as follows:

$function = 'my_awesome_page_display';

'my_awesome_page_display' is a callback function name that you’re expected to define, and which will contain the markup that displays on your page.

To start, you can just use something pretty simple like:

function my_awesome_page_display() {
    echo '<h1>My Page!!!</h1>';
}

And with that, you’ve created a really really useless admin page. It provides no options and doesn’t display any data. It just says “My Page!!!” at the top.

What you probably want to do is create a <form> on that page so people can give your plugin or theme useful data, and which’ll probably end up being stored in the database as WordPress options. This is where the WordPress Settings API can be really helpful, and justify all its complexity.

Consider Breaking Your Form Out Into a Separate File

This is something you should consider whether or not you use the Settings API to build your form: rather than making your callback function 100 lines long, include a separate template file. You can simplify things really nicely by not filling your callback function with a bunch of HTML echos.

Creating your form and the rest of your markup in a separate file that you include looks something like this:

// in main file
function my_awesome_page_display() {
    include 'form-file.php';
}
// form-file.php
<h1>My Awesome Settings Page</h1>

This has the big advantage that, in our main settings page file, we’re not juggling a bunch of echoes or a bunch of open-and-closing PHP tags.

3. Creating a Working Form and Saving Your Settings

At the moment, our page remains useless. Here’s some actually working code that’ll give you a working admin options page, without using the Settings API. It also begins to highlight some of the reasons that you’ll want to use the Settings API.

// in main file
function my_awesome_page_display() {
    if (isset($_POST['awesome_text'])) {
        update_option('awesome_text', $_POST['awesome_text']);
        $value = $_POST['awesome_text'];
    } 

    $value = get_option('awesome_text', 'hey-ho');

    include 'form-file.php';
}
// form-file.php
<h1>My Awesome Settings Page</h1>

<form method="POST">
    <label for="awesome_text">Awesome Text</label>
    <input type="text" name="awesome_text" id="awesome_text" value="<?php echo $value; ?>">
    <input type="submit" value="Save" class="button button-primary button-large">
</form>

What does this do? As we just covered, form-file.php is what’s displaying the form, which we’ve just added—it’s all the markup inside <form>.

Note that we haven’t set an action value on our form. That will make the form use its method—which we did specify, as POST—back to the same page it was on.

This form has only one field and doesn’t do much. Its only field is named awesome_text, and it does get a dynamically filled value of $value from PHP. That’ll be supplied from the location of our function call which includes the template.

Speaking of that function, at the bottom it’s assigning a value to the variable $value. PHP makes variable values available to included files, which is why our form knows about $value.

Note that getting $value is done using a get_option() call. If you’ve read about the Options API, you may remember that the first parameter is the name of your option and the second is the (optional) value you want back if your option isn’t set. So you could rewrite our get_option() call above in two ways:

$value = get_option('awesome_text', 'hey-ho');

// or

$value = get_option('awesome_text');
if (FALSE === $value) {
    $value = 'hey-ho';
}

They work the same, but because I think the signature and meaning of the get_option call is readable and obvious enough, I prefer the shortness of the first.

The logic at the top of the function uses the PHP $_POST superglobal array to get the submitted value of the form. If you’re not familiar with $_POST, the very basic thing you need to understand is that PHP pulls out values from submitted forms POSTed to a script, and puts them in the $_POST array for you, at the index that was the form elements’s name field. In our case, the form element was named awesome_text, and we get that value by accessing $_POST['awesome_text'].

Finally, the if-statement at the beginning of my_awesome_page_display() is again using the Options API, this time to save $_POST['awesome_text'], if it’s set, as the new value for awesome_text in the database.

Security Considerations of Saving Options

The above code works and is valid, but it has three causes for caution from a security perspective:

  1. We’re not checking that the submitted value came from a user who has permissions to change the option
  2. We’re not making sure that after such a check, the user intended to change the specified form (via a nonce)
  3. We’re not making sure that the submitted data has the right shape, and doesn’t contain something malicious

The details of why these are important can be understood a little better in our primer on WordPress security, but the basic code that would improve these first two problems are:

// form-file.php
<h1>My Awesome Settings Page</h1>

<form method="POST">
    <label for="awesome_text">Awesome Text</label>
    <input type="text" name="awesome_text" id="awesome_text" value="<?php echo $value; ?>">
    <?php echo wp_nonce_field( 'wpshout_option_page_example_action' ); ?>
    <input type="submit" value="Save" class="button button-primary button-large">
</form>

And for the saving:

// in main file
function my_awesome_page_display() {
    if (!current_user_can('manage_options')) {
        wp_die('Unauthorized user');
    }

    if (!wp_verify_nonce( '_wp_nonce', 'wpshout_option_page_example_action' )) {
        wp_die('Nonce verification failed');
    }

    if (isset($_POST['awesome_text'])) {
        update_option('awesome_text', $_POST['awesome_text']);
        $value = $_POST['awesome_text'];
    } 

    $value = get_option('awesome_text', 'hey-ho');

    include 'form-file.php';
}

What we’ve changed in the above code is make changes so that, first, the user is checked. A user could potentially forge a request to our form and have non-admins edit the options. We don’t want that to happen, so we’re first making sure before we save submitted data that the user has the right permissions.

After that, we’re making sure that the nonce we added to the form matches. This makes sure that a user who has the right permissions can’t be maliciously tricked into changing our option by accident. In both cases, you may want to consider a better and more elegant handling of a guard-cluase failure than my wp_dies, but these will work to keep your users safe. (You may well frustrate them though.)

A last security concern worth noting is the possibility of invalid or malicious data. If our option is, for example, supposed to be an integer, it is a good idea to make sure that the value is an integer. register_setting is one of the ways to accomplish this sanitization, but you could also just do it here. The whole topic of sanitization and validation is too big for this article, but definitely keep it in mind.

Drawbacks of Skipping the Settings API

Playing with the $_POST variable isn’t inherently a problem, but it sure isn’t pretty.

One of the biggest drawbacks to what we’ve done here — just set our options ourselves using the Options API — is that we’ve had to write some pretty boilerplate stuff ourselves. Playing with the $_POST variable isn’t inherently a problem, but it sure isn’t pretty.

A more practical problem is one veterans of the internet may guess. Because of the way this form was built, if a user (or their browser) reloads the page, you’ll get a prompting about “Do you want to resubmit form data?” This is one of those little things that’s navigable with your own PHP—you’ll want to use the Post/Redirect/Get pattern—but it’s not something you’re faced with when using the Settings API. Our friend Carl published a great piece about that:

Preventing Form Resubmission Warnings in WordPress with the Post/Redirect/Get Pattern

What We’ve Learned

The biggest and most generalizable lesson we’ve covered is how you create new admin pages for your plugin or theme. In either situation, you want to use those add_*_page() functions, picking the one most appropriate for those situations.

In each of those functions, you’ll need to passing in the name of a callback function which you’ll use to actually create your page and its form. In our example today, we used some old-school PHP, and WordPress’s Options API, to process that form submission. But if you do choose to use the Settings API, you’d still register that page the same way.

There are arguments worth having about whether or not the Settings API is appropriate and useful. But whether you end up using it or not, you now know the very essentials of creating a WordPress admin page—and that’s nothing to sneeze at. Happy hacking!

Image credit: Oliver Tacke


17 Responses

Comments

  • Mick Levin says:

    Nice intro article.

    With WP4 however, it’s time to switch to Customizer API, it’s way simplier and allows preview changes while making them.

    • The customizer’s great for visual things. But it’s hardly a complete one-for-one replacement with other ways to give a plugin feedback about how it should behave.

  • I’m not a huge fan of doing settings pages “The WordPress Way” either. That said, skipping register_setting(), and any sanitization of POST data at all, is a security risk, and is not a step worth skipping.

  • Sarmen B. says:

    thank you!. i’d rather sanitize teh data myself then having to comprehend the settings api. i have 8 yrs of php and for some reason i cant wrap my head around learning the settings api.

  • Shrinivas says:

    It is a useful article. How to add the tabbed setting pages for settings panel built with wordpress settings API?

  • Thank you for this tut man …very helpful

  • Mulli says:

    I wish I saw this article before…

    I am using the setting API hoping that it will keep my admin updated with WP versions
    BUT, its complex, simply the wrong way to go.

    Soon I will replace using this article to hold my hand…
    BIG LIKE and thanks

  • Cloud says:

    No offense! Settings API is not pretty even tedious, but your way is worse. The Options page creating with Settings API is pluginable, extensible and flexiable, imagine that you created a plugin and an options page for this plugin, then other users or developers want to add extra settings fields or sections to the options page, it’s impossible by your way unless modify your code directly, but with Settings API, we can add/remove/modify a settings sections or fields without modify original file/codes.

  • DW says:

    In the main file what is the purpose of this line:

    $value = $_POST[‘awesome_text’];

    when the next thing you do, immediately after the if statement, is another assignment with the same variable?

    $value = get_option(‘awesome_text’, ‘hey-ho’);

    It seems to me you could remove $value = $_POST[‘awesome_text’];

  • Anders says:

    Hi there!

    1. I belive wp_nonce_field echo as default, which means the preceding echo are unnecessary.

    2. You are executing wp_verify_nonce even without knowing that the request was a POST. This if-logic will effectively prevent all users from ever reaching the option page.

    Sorry if I have made a mistake and my points are incorrect.
    Nice article on a different approach although !

    • Anders says:

      3. The default name of the nonce should be _wpnonce, not _wp_nonce

      4. If you use check_admin_referer instead of wp_verify_nonce you can skip the surrounding if-logic.

  • Jason Lemahieu says:

    Have you tried using Advanced Custom Fields for settings page?

    https://www.advancedcustomfields.com/resources/options-page/

    • Newton says:

      Hey Jason!

      ACF is great, but we have to be careful.

      Using ACF you can…
      – Increase by 173% to render page
      – Increase by 14% your number of queries
      – Increase by 26% memory usage

      You really should consider the advantages and disadvantages of using ACF.

      See ya!

  • Hi, I have 1 setting page from other them can i add it to wordpress current them through calling a page only

    function my_setting_page_option_display() {
    include ‘form-file.php’;
    }

    or it work only through admin_int function

    I get many times code error while modifying new setting

  • hi, Please make me clear can i hook that customiser setting to anther theme.

  • Mohamed says:

    Thank you for your efforts could you please send me the code because the final result is Nonce verification failed

  • I did it that way
    first i check if post is not empty.

    if (!empty($_POST))
    {
    if (! isset( $_POST[‘name_of_nonce_field’] ) || ! wp_verify_nonce( $_POST[‘name_of_nonce_field’], ‘name_of_my_action’ ) )
    {
    print ‘Sorry, your nonce did not verify.’;
    exit;
    }
    }

    ….form….
    echo wp_nonce_field( ‘name_of_my_action’, ‘name_of_nonce_field’ );
    …/form….

Add a comment

(required)

(required)

(optional)