Creating menus – WordPress Plugin Development [Part 2]

This is the second part of my WordPress Plugin Development tutorial. Check also the first part.

1. Adding a menu item:

A menu item is created by calling the add_options_page() method, which should be called inside a admin_menu action.

This adds a menu item in the “Settings” menu. For other menus:

  • Dashboard: add_dashboard_page()
  • Posts: add_posts_page()
  • Media: add_media_page()
  • Pages: add_pages_page()
  • etc

They all have the same syntax as the Settings menu.

Now that you know the basics of menu creation, let’s see how to create a settings page. But don’t worry, we are going to see advanced menu topics later (custom menus, icons, sub-menus).

2. Admin page HTML:

So far we have created this simple HTML page for our menu on my_plugin_options():

Let’s now replace it with a form that saves data on the database:

  • settings_fields( $option_group ): Creates some hidden fields:
  • do_settings_sections( $option_group ): Renders all the HTML inputs. We are going to define this next.
  • submit_button(): Renders the submit button.


Each group option is a row on the wp_options table. While is possible to save each setting as a row, this clutters the table. Instead, we group options and save them together in just one row. The $option_group value is the id of the row.

You can use more than one option group in your plugin - just try to not use many of them, one per menu item is a good choice.


Fields are visually grouped in sections. Let's define some:

The result:

The first argument is the section id; the second argument is the title; the third argument is the callback; the fourth is the option group.


Now, let's define some fields for each section:


If you click on save, an "ERROR: options page not found" will happen:

To fix this, we need to register our fields with register_setting():

And let's show the success message with settings_errors():

Now, click to save it again and voila:

Retrieving data:

Data is being saved, but not being showed. Let's fix it with get_option( $field ):

As result, it now shows the current values:

Escape the output:

It is a good practice to strip invalid or special characters before output. For this, use the esc_attr( $text ) function.

esc_attr( get_option( 'field1' ) )

4. Validation and Sanitization:

Let's say our first field is an email and we want to validate it.

The 3rd argument of register_setting() is the sanitization callback. The callback should return the value that will be saved:

register_setting( 'my-menu', 'field1', [ $this, 'validation_callback1' ] );

public function validation_callback1( $input ) {
  return sanitize_email( $input );

Now, let's throw an error message with add_settings_error( string $setting, string $code, string $message, string $type = 'error' ):

public function validation_callback1( $input ) {
  /** Sanitize input */
  $sanitized = sanitize_email( $input );

  /** If output is different, input was wrong */
  if ( $sanitized !== $input ) {
    add_settings_error( 'field1', 'your-error-code', 'Field 1 is not valid!' );

  /** Save the sanitized version */
  return $sanitized;

5. Permissions:

It is a good practice to check if the user has permissions to access the page. You can use the is_admin() or current_user_can( $capability ) methods for this:

6. Drying the code:

Defining setting fields can be a repetitive task. And it doesn't help that each input requires its own method.

So, let's refactor our example and DRY (don't-repeat-yourself) our code.

First, let's use a generic method for the field rendering:


The add_settings_field() method accepts an extra argument that is sent to the callback:

add_settings_field( $id, $title, $callback, $page, $section, $args )

Let's pass in this argument all the information necessary to build the HTML element:

Now, let's refactor the field_callback() to generate the field with this information:

Final code:

Finally, let's structure the sections/fields in an array and automate the process:

7. Custom menu:

For a custom menu, use the add_menu_page() method instead:

/** Step 2 (add item). */
public function my_plugin_menu() {
  $page_title = 'My Plugin Options';
  $menu_title = 'My Plugin';
  $capability = 'manage_options'; // which menu.
  $menu_slug  = 'my-menu'; // unique identifier.
  $callback   = [ $this, 'my_plugin_options' ];
  add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $callback );

This function has two extra optional arguments: icon and position:

Custom icon:

$icon       = 'dashicons-media-code';
add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $callback, $icon );

The complete list of icons:

For a custom icon, use: plugins_url( 'my-plugin/icon.png' ). You can also pass a base64-encoded SVG. Alternatively, pass 'none' and set your icon on the CSS.

Custom position:

The last argument, also optional, is the menu position. By default, it will be placed at the bottom, but you can change this.

Position values:

  • 2 – Dashboard
  • 4 – Separator
  • 5 – Posts
  • 10 – Media
  • 15 – Links
  • 20 – Pages
  • 25 – Comments
  • 59 – Separator
  • 60 – Appearance
  • 65 – Plugins
  • 70 – Users
  • 75 – Tools
  • 80 – Settings
  • 99 – Separator

For example, position 76 will place it between Tools and Settings.

$position   = 76;
add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $callback, $icon, $position );

8. Sub-menus:

To add sub-menus, use the add_submenu_page( ... ) method.

The first argument is the parent menu slug. The other arguments are the same as the other methods:


Notice that the first item has the same title as the menu. That's the default behavior on WordPress.

Setting a different title for the first sub-menu item:

To fix it, add an item with the same slug as the parent menu.

Close Menu