Skip to content
ExponaBLOG
WORDPRESS DEVELOPMENT & PLUGINS · DAY 015 / 100

Settings pages with the Settings API: a practical guide

Admin settings done properly.

Expona Admin7 min read

Why Settings Pages Matter

Building a WordPress plugin is one thing. Building a plugin that administrators can actually configure without touching code is another. Today on day 15 of our 100-day WordPress Development & Plugins series, we're tackling Settings pages with the Settings API: a practical guide to help your admin teams manage plugin behavior in production. When you wire up your settings properly, you reduce support tickets, enable non-technical site owners to customize behavior, and give your plugin the professionalism it deserves.

The WordPress Settings API provides a standardized way to build admin pages that handle option registration, sanitization, validation, and UI rendering. It's been part of WordPress for years, and for good reason: it abstracts away nonces, sanitization, and escaping, letting you focus on what your plugin actually does.

Register Settings — register_setting with a Sanitize Callback

Before your admin interface can save anything, you need to register the option with WordPress using register_setting(). This is where the Settings API begins and where you establish trust boundaries around user input.

Every option in WordPress is a key-value pair stored in the wp_options table. When you register a setting, you're telling WordPress: "This is a real, validated setting. Here's how to handle it when someone tries to save it." The sanitize callback is your gatekeeper.

php
function my_plugin_register_settings() {
    register_setting(
        'my_plugin_options_group',      // option group
        'my_plugin_settings',           // option name
        array(
            'type'              => 'array',
            'sanitize_callback' => 'my_plugin_sanitize_settings',
            'show_in_rest'      => false,
            'default'           => array()
        )
    );
}

add_action( 'admin_init', 'my_plugin_register_settings' );

function my_plugin_sanitize_settings( $settings ) {
    if ( ! is_array( $settings ) ) {
        return array();
    }

    $sanitized = array();

    if ( isset( $settings['api_key'] ) ) {
        $sanitized['api_key'] = sanitize_text_field( $settings['api_key'] );
    }

    if ( isset( $settings['max_retries'] ) ) {
        $sanitized['max_retries'] = absint( $settings['max_retries'] );
    }

    if ( isset( $settings['enable_logging'] ) ) {
        $sanitized['enable_logging'] = rest_sanitize_boolean( $settings['enable_logging'] );
    }

    return $sanitized;
}

The sanitize callback receives whatever data came from the form and returns clean, validated data that WordPress will store. Always assume the input is hostile. Use sanitize_text_field() for strings, absint() for integers, rest_sanitize_boolean() for checkboxes, and so on. The callback runs on every save, making it your first line of defense.

Notice we run this on the admin_init hook, not init. This ensures registration only happens in the admin, keeping your frontend uncluttered.

Sections and Fields — add_settings_section and add_settings_field

Once you've registered your setting, you need to build the UI that lets admins change it. The Settings API gives us add_settings_section() and add_settings_field() to do this in a clean, organized way.

Think of sections as container groups and fields as individual inputs within those groups. A typical plugin might have a section for "API Configuration" with fields for the API key, endpoint URL, and timeout values. Another section might be "Logging" with a checkbox for debug mode and a field for log retention days.

php
function my_plugin_add_settings_sections() {
    add_settings_section(
        'my_plugin_api_section',        // section ID
        'API Configuration',            // section title
        'my_plugin_api_section_callback',  // callback for description
        'my_plugin_settings_page'       // page slug (matches do_settings_sections())
    );

    add_settings_section(
        'my_plugin_logging_section',
        'Logging & Debug',
        'my_plugin_logging_section_callback',
        'my_plugin_settings_page'
    );
}

add_action( 'admin_init', 'my_plugin_add_settings_sections' );

function my_plugin_api_section_callback() {
    echo 'Configure your external API connection below.';
}

function my_plugin_logging_section_callback() {
    echo 'Control logging verbosity and retention.';
}

Now add the fields into those sections:

php
function my_plugin_add_settings_fields() {
    $options = get_option( 'my_plugin_settings', array() );

    add_settings_field(
        'my_plugin_api_key',            // field ID
        'API Key',                      // field label
        'my_plugin_api_key_callback',   // callback to render input
        'my_plugin_settings_page',      // page slug
        'my_plugin_api_section'         // section ID
    );

    add_settings_field(
        'my_plugin_max_retries',
        'Max Retries',
        'my_plugin_max_retries_callback',
        'my_plugin_settings_page',
        'my_plugin_api_section'
    );

    add_settings_field(
        'my_plugin_enable_logging',
        'Enable Debug Logging',
        'my_plugin_enable_logging_callback',
        'my_plugin_settings_page',
        'my_plugin_logging_section'
    );
}

add_action( 'admin_init', 'my_plugin_add_settings_fields' );

function my_plugin_api_key_callback() {
    $options = get_option( 'my_plugin_settings', array() );
    $api_key = isset( $options['api_key'] ) ? $options['api_key'] : '';
    ?>
    
    
    
    
    
    />
    Enable verbose logging to the database
    

Each field callback renders its own HTML input. The key pattern is naming inputs as my_plugin_settings[field_name], which allows WordPress to group them under a single option when the form submits. Your sanitize callback then unpacks this array.

With settings registered and fields defined, you need somewhere for admins to see and edit them. Use add_options_page() to add a submenu item under the WordPress Settings menu, or add_menu_page() to create a top-level menu item for your plugin.

php
function my_plugin_add_admin_menu() {
    add_options_page(
        'My Plugin Settings',           // page title
        'My Plugin',                    // menu title
        'manage_options',               // capability required
        'my_plugin_settings_page',      // menu slug
        'my_plugin_settings_page_html'  // callback to render page
    );
}

add_action( 'admin_menu', 'my_plugin_add_admin_menu' );

function my_plugin_settings_page_html() {
    // Check user capability
    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die( 'You do not have permission to access this page.' );
    }
    ?>
    <div>
        <h1></h1>
        
            
        
    </div>
    

The three critical functions here are:

  • settings_fields() outputs nonce and option group hidden fields.
  • do_settings_sections() renders all sections and fields registered for that page slug.
  • submit_button() renders the standard "Save Changes" button.

WordPress automatically handles the form submission and calls your sanitize callback. No manual SQL queries, no extra validation boilerplate.

Capability Checks — manage_options for Site-Wide Settings

Every admin page should check user capabilities before rendering. The standard capability for site-wide settings is manage_options, which only administrators have by default.

php
function my_plugin_add_admin_menu() {
    add_options_page(
        'My Plugin Settings',
        'My Plugin',
        'manage_options',  // Only admins can see this page
        'my_plugin_settings_page',
        'my_plugin_settings_page_html'
    );
}

function my_plugin_settings_page_html() {
    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die( 'You do not have permission to access this page.' );
    }
    // ... rest of page
}

If your plugin manages per-post settings or plugin-specific roles, you might create a custom capability like manage_my_plugin_settings and map it to roles as needed. But for most plugins, manage_options is correct.

Always check capabilities both when registering the menu item and at the start of your page callback. Defense in depth.

Common Mistakes

Forgetting the sanitize callback. Without it, you're storing unsanitized user input directly to the database. If that setting ever gets echoed on the frontend without escaping, it's an XSS vulnerability.

Mixing option names and group names. The "option group" passed to register_setting() is not the same as the option name. The group is used for nonce verification and grouping related settings. The option name is what gets stored in wp_options. Get them mixed up and forms won't save.

Calling register_setting on init instead of admin_init. This loads unnecessary code on the frontend and can cause issues with some plugin combinations.

Not escaping when rendering. Use esc_attr() in input values, esc_html() for text content. Even though you sanitized on input, escaping on output is a separate safety layer.

Forgetting settings_fields(). This outputs the nonce and option group hidden fields. Without it, the form won't process correctly and your data won't save.

Key Takeaways

  • Use register_setting() with a robust sanitize callback on the admin_init hook to establish a trust boundary for your options.
  • Organize your UI with add_settings_section() and add_settings_field(), grouping related options logically for the admin.
  • Create a menu page with add_options_page() or add_menu_page(), always checking manage_options capability.
  • Always call settings_fields(), do_settings_sections(), and submit_button() in your page template.
  • Sanitize on input, escape on output, and verify capabilities at every entry point.
  • Test your settings page in a staging environment; a misconfigured option group or missing nonce can silently break form submission.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles