Himanshu SharmaSoftware engineer
Let’s talk

WordPress · Lesson 34 of 45

Options and settings pages

Store site-wide settings with the Options API and build a settings page with the Settings API, including sections, fields, sanitising and capabilities.

15 minHooks and pluginsUpdated 23 October 2026

By the end of this lessonSave a plugin's settings safely with get_option() and update_option(), and add an admin settings page that validates what users type.

Settings that belong to the site

Post meta belongs to one post. A setting belongs to the whole site: a plugin's API key, the number of items to show, a colour. WordPress stores such values as options in the wp_options table. WordPress's own Settings screens (General, Reading, Permalinks) are built on the same options you can read from code.

The Options API

php
<?php
// Read: the second argument is what you get if the option does not exist.
echo get_option( 'blogname' ), "\n";
var_dump( get_option( 'bakery_items', 5 ) );

// Create and change.
add_option( 'bakery_items', 8 );          // only adds if missing
update_option( 'bakery_items', 10 );      // adds or changes
var_dump( get_option( 'bakery_items' ) );

// Several values together: store one array option instead of many.
update_option( 'bakery_settings', [ 'items' => 10, 'show_prices' => true ] );
print_r( get_option( 'bakery_settings' ) );

delete_option( 'bakery_items' );
var_dump( get_option( 'bakery_items', 'gone' ) );
result
My WordPress Site
int(5)
int(10)
Array
(
    [items] => 10
    [show_prices] => 1
)
string(4) "gone"

Guidelines:

  • Prefix option names with your plugin name.
  • Options are loaded into memory on every request when "autoloaded" (the default). Keep autoloaded options small; pass false as the third argument of add_option() for big or rarely used values.
  • Use one array option for related settings of a plugin, instead of ten separate rows.
  • Options are not for per-post data (use post meta) or for temporary cached values (use transients).

From the command line, wp option get, wp option update and wp option list do the same.

The Settings API

You could build a settings form by hand, but then you must handle the nonce, the capability, saving, and the "Settings saved" notice. The Settings API does that for you. You describe the setting, a section and the fields, and WordPress draws and saves them.

php
<?php
/**
 * Plugin Name: Bakery Settings
 */
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// 1. Register the setting with a sanitise callback.
add_action( 'admin_init', function () {
	register_setting( 'bakery_group', 'bakery_settings', [
		'type'              => 'array',
		'sanitize_callback' => 'bakery_sanitize_settings',
		'default'           => [ 'items' => 5, 'shop_name' => '' ],
	] );

	// 2. A section and its fields.
	add_settings_section( 'bakery_main', 'Shop display', '__return_false', 'bakery-settings' );
	add_settings_field( 'items', 'Items to show', 'bakery_field_items', 'bakery-settings', 'bakery_main' );
	add_settings_field( 'shop_name', 'Shop name', 'bakery_field_name', 'bakery-settings', 'bakery_main' );
} );

function bakery_sanitize_settings( $input ): array {
	$input = is_array( $input ) ? $input : [];
	$items = isset( $input['items'] ) ? absint( $input['items'] ) : 5;
	return [
		'items'     => min( max( $items, 1 ), 50 ),                       // keep it between 1 and 50
		'shop_name' => sanitize_text_field( $input['shop_name'] ?? '' ),  // plain text only
	];
}

function bakery_field_items() {
	$options = get_option( 'bakery_settings' );
	printf( '<input type="number" name="bakery_settings[items]" value="%d" min="1" max="50">', (int) ( $options['items'] ?? 5 ) );
}

function bakery_field_name() {
	$options = get_option( 'bakery_settings' );
	printf( '<input type="text" name="bakery_settings[shop_name]" value="%s" class="regular-text">', esc_attr( $options['shop_name'] ?? '' ) );
}

// 3. The menu entry and the page.
add_action( 'admin_menu', function () {
	add_options_page( 'Bakery Settings', 'Bakery', 'manage_options', 'bakery-settings', 'bakery_settings_page' );
} );

function bakery_settings_page() {
	if ( ! current_user_can( 'manage_options' ) ) {
		return;
	}
	?>
	<div class="wrap">
		<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
		<form action="options.php" method="post">
			<?php
			settings_fields( 'bakery_group' );          // nonce and hidden fields
			do_settings_sections( 'bakery-settings' );  // our section and fields
			submit_button();
			?>
		</form>
	</div>
	<?php
}
bash
wp plugin activate bakery-settings
result
Plugin 'bakery-settings' activated.
Success: Activated 1 of 1 plugins.

Three parts:

  1. register_setting() names the option, its type, a default and, most importantly, the sanitise callback. WordPress calls it every time the option is saved through the form.
  2. Sections and fields are described with add_settings_section() and add_settings_field(); each field has a function that prints its input. Name inputs option_name[key] so everything arrives as one array.
  3. The page is added to the Settings menu with add_options_page(), restricted to manage_options. The form posts to options.php, and settings_fields() adds the nonce and hidden fields that make saving secure. You do not write a save handler.

Sign in and see the page

The page is in the dashboard, so log in first. WordPress sets a test cookie when it shows the login form, then the form posts the username and password and answers with a redirect to the dashboard:

http
GET /wp-login.php
http
POST /wp-login.php
Content-Type: application/x-www-form-urlencoded

log=admin&pwd=password&wp-submit=Log+In&redirect_to=%2Fwp-admin%2F&testcookie=1
result
HTTP/1.1 302 Found
Location: /wp-admin/

The cookies WordPress sends back identify you on later requests, and now the settings page opens:

http
GET /wp-admin/options-general.php?page=bakery-settings
result
Bakery Settings
http
GET /wp-admin/options-general.php?page=bakery-settings
result
<input type="number" name="bakery_settings[items]" value="10" min="1" max="50">
<input type="text" name="bakery_settings[shop_name]" value="" class="regular-text">

What the sanitise callback does

When the form is submitted, options.php passes the posted values through the callback before saving. You can see what it does by calling it with messy input:

php
<?php
print_r( bakery_sanitize_settings( [ 'items' => '500', 'shop_name' => '  <b>Daily</b> Bread  ' ] ) );
print_r( bakery_sanitize_settings( [ 'items' => 'abc', 'shop_name' => "Line one\nLine two" ] ) );
print_r( bakery_sanitize_settings( 'not even an array' ) );
result
Array
(
    [items] => 50
    [shop_name] => Daily Bread
)
Array
(
    [items] => 1
    [shop_name] => Line one Line two
)
Array
(
    [items] => 5
    [shop_name] =>
)

500 became 50, the HTML tags were removed from the name and surplus whitespace trimmed, a non-number fell back to the minimum, and a wrong kind of input became a valid array. Always validate in the callback, because anyone who can post to options.php can send any values. (WordPress attaches the callback when the dashboard loads, which is why code run from the command line must call it directly or use update_option( 'bakery_settings', bakery_sanitize_settings( $values ) ).)

Showing your settings on the site

php
<?php
update_option( 'bakery_settings', bakery_sanitize_settings( [ 'items' => '12', 'shop_name' => 'Daily Bread' ] ) );

function bakery_get_setting( string $key ) {
	$options = wp_parse_args( get_option( 'bakery_settings', [] ), [ 'items' => 5, 'shop_name' => '' ] );
	return $options[ $key ] ?? null;
}
echo esc_html( bakery_get_setting( 'shop_name' ) ), ' shows ', (int) bakery_get_setting( 'items' ), " items\n";
result
Daily Bread shows 12 items

wp_parse_args() fills in defaults for missing keys. Escape on output (esc_html(), esc_attr()), even for values you sanitised on the way in.

Other places for settings

  • Customizer and Site Editor: theme options belong in theme.json and the Site Editor for block themes.
  • Block attributes: options for a single block belong in the block.
  • wp-config.php constants: for secrets and environment settings you do not want editable in the admin.

Common mistakes

  • No sanitize_callback, so any value can be stored.
  • Printing option values without escaping.
  • Many separate options for one plugin, all autoloaded.
  • Building your own save handler without nonce and capability checks. Use the Settings API.
  • Showing the settings page to everyone (missing capability check).

Exercise

Add a checkbox setting show_prices to the form. Sanitise it as a boolean, so unchecked boxes (which are not submitted at all) save as false.

Show one possible answer
php
// in bakery_sanitize_settings():
'show_prices' => ! empty( $input['show_prices'] ),

// the field:
function bakery_field_prices() {
	$options = get_option( 'bakery_settings' );
	printf( '<input type="checkbox" name="bakery_settings[show_prices]" value="1" %s>', checked( ! empty( $options['show_prices'] ), true, false ) );
}
add_settings_field( 'show_prices', 'Show prices', 'bakery_field_prices', 'bakery-settings', 'bakery_main' );

An unchecked checkbox sends nothing, so ! empty( $input['show_prices'] ) gives false.

Key points

  • Options are site-wide key/value settings stored in the wp_options table; use get_option(), add_option(), update_option() and delete_option().
  • The Settings API (register_setting, add_settings_section, add_settings_field) builds a settings page and saves it for you with a nonce and capability check.
  • Give every setting a sanitize_callback so only valid values are stored, and escape values when you print them.
  • Add the page with add_options_page() or add_menu_page() and require a capability such as manage_options.

Check yourself

1. Where are options stored?
2. What does the third argument to get_option( 'key', 'fallback' ) do?
3. What is the purpose of sanitize_callback in register_setting()?
4. Which capability normally controls who can open a plugin's settings page?