How to create an options page with the Settings API in PHP in WordPress

Contents

Overview

This tutorial explains, in complete detail, how to create an admin options page in WordPress using the Settings API in PHP. It covers concepts, required functions, a step-by-step example plugin that registers an options page, registers settings/sections/fields, sanitizes input, uses the WordPress color picker and media uploader, displays settings errors, and demonstrates best practices for security and compatibility.

Why use the Settings API?

  • Security and consistency: The Settings API automatically handles nonces and options storage patterns when used correctly.
  • Separation of concerns: You register settings and field callbacks and let WordPress output form markup with settings_fields() and do_settings_sections().
  • Built-in validation hooks: register_setting() accepts a sanitize callback for centralized validation and sanitization.
  • Scalability: Settings can be grouped into sections, and many fields can be added systematically.

Key Settings API functions and terminology

register_setting Register an option name, optional sanitize callback, and associates it with a settings group.
add_settings_section Define a logical section on the options page (title and description).
add_settings_field Register a specific field (label and callback) associated with a section.
settings_fields Outputs hidden fields including the nonce and option_group value. Place inside your form.
do_settings_sections Outputs all sections and fields registered for a given settings page.
submit_button Outputs the standard Save Changes button.
get_option Retrieve the saved option(s).
add_settings_error settings_errors Programmatically add and display admin notices for settings validation, success, or errors.

Prerequisites and notes

  • WordPress 4.7 is recommended Settings API has been available for many versions, but APIs like the color picker and media uploader rely on current admin scripts.
  • The user managing settings should have the capability to manage_options (typically administrators).
  • Always sanitize and escape data. register_setting()s sanitize callback should return sanitized data or use add_settings_error() for invalid values.
  • Use esc_attr(), esc_html(), esc_textarea(), checked(), selected() when outputting to HTML.

Step-by-step example: Complete plugin demonstrating Settings API

Below is a complete example plugin that:

  • Registers an options page under Settings.
  • Registers one option (an array) with default values, a sanitize callback, one section, and a variety of field types (text, textarea, checkbox, select, radio, color, image upload).
  • Enqueues necessary admin scripts for color picker and media uploader.
  • Shows how to read options with get_option() and output safely.
lt?php
/
Plugin Name: Example Settings API Options Page
Description: Demonstrates creating an options page with the WordPress Settings API
Version: 1.0
Author: Example Author
/

if ( ! defined( ABSPATH ) ) {
    exit
}

class Example_Settings_API_Plugin {

    private option_name = example_settings_api_options
    private defaults = array(
        text_field   =gt ,
        textarea     =gt ,
        checkbox     =gt 0,
        select       =gt option1,
        radio        =gt yes,
        color        =gt #ffffff,
        image_id     =gt 0,
    )

    public function __construct() {
        add_action( admin_menu, array( this, add_admin_menu ) )
        add_action( admin_init, array( this, register_settings ) )
        add_action( admin_enqueue_scripts, array( this, enqueue_admin_assets ) )
    }

    public function add_admin_menu() {
        // Add an options page under Settings
        add_options_page(
            Example Settings,
            Example Settings,
            manage_options,
            example-settings-api,
            array( this, options_page_html )
        )
    }

    public function register_settings() {
        // Register the option with a sanitize callback
        register_setting(
            example_settings_group, // option group (settings_fields() uses this)
            this->option_name,      // option name in wp_options
            array( this, sanitize_callback ) // sanitize callback
        )

        // Add a section
        add_settings_section(
            example_main_section,
            Main Settings,
            array( this, section_text ),
            example-settings-api
        )

        // Add fields (each fields callback outputs the input HTML)
        add_settings_field(
            text_field,
            Text Field,
            array( this, render_text_field ),
            example-settings-api,
            example_main_section
        )

        add_settings_field(
            textarea,
            Text Area,
            array( this, render_textarea_field ),
            example-settings-api,
            example_main_section
        )

        add_settings_field(
            checkbox,
            Enable Option,
            array( this, render_checkbox_field ),
            example-settings-api,
            example_main_section
        )

        add_settings_field(
            select,
            Select Choice,
            array( this, render_select_field ),
            example-settings-api,
            example_main_section
        )

        add_settings_field(
            radio,
            Radio Choice,
            array( this, render_radio_field ),
            example-settings-api,
            example_main_section
        )

        add_settings_field(
            color,
            Color Picker,
            array( this, render_color_field ),
            example-settings-api,
            example_main_section
        )

        add_settings_field(
            image,
            Image Upload,
            array( this, render_image_field ),
            example-settings-api,
            example_main_section
        )
    }

    public function enqueue_admin_assets( hook ) {
        // Only enqueue on our options page
        if ( hook !== settings_page_example-settings-api ) {
            return
        }

        // Color picker
        wp_enqueue_style( wp-color-picker )
        wp_enqueue_script( wp-color-picker )

        // Media uploader
        wp_enqueue_media()

        // Our inline script to instantiate color picker and media uploader
        script = 
        jQuery(document).ready(function(){
            // Color picker
            (.example-color-field).wpColorPicker()

            // Media uploader for image field
            var file_frame
            (document).on(click, .example-upload-button, function(e){
                e.preventDefault()
                var button = (this)
                var input = button.prev(.example-image-id)
                var preview = button.siblings(.example-image-preview)

                if (file_frame) {
                    file_frame.open()
                    return
                }

                file_frame = wp.media.frames.file_frame = wp.media({
                    title: Select Image,
                    button: { text: Use this image },
                    multiple: false
                })

                file_frame.on(select, function() {
                    var attachment = file_frame.state().get(selection).first().toJSON()
                    input.val(attachment.id)
                    if (preview.length) {
                        preview.attr(src, attachment.sizes  attachment.sizes.thumbnail ? attachment.sizes.thumbnail.url : attachment.url)
                    }
                })

                file_frame.open()
            })

            (document).on(click, .example-remove-image, function(e){
                e.preventDefault()
                var button = (this)
                var input = button.prevAll(.example-image-id)
                var preview = button.siblings(.example-image-preview)
                input.val()
                if (preview.length) {
                    preview.attr(src, )
                }
            })
        })
        

        wp_add_inline_script( wp-color-picker, script )
    }

    public function section_text() {
        echo 

Configure the example plugin settings below.

} private function get_options_with_defaults() { options = get_option( this->option_name, array() ) if ( ! is_array( options ) ) { options = array() } return wp_parse_args( options, this->defaults ) } // Field render callbacks public function render_text_field() { options = this->get_options_with_defaults() echo ltinput type=text name= . esc_attr( this->option_name ) . [text_field] value= . esc_attr( options[text_field] ) . class=regular-text /> echo ltp class=description>A single-line text field.

} public function render_textarea_field() { options = this->get_options_with_defaults() echo lttextarea name= . esc_attr( this->option_name ) . [textarea] rows=6 cols=50> . esc_textarea( options[textarea] ) . lt/textarea> echo ltp class=description>A multi-line textarea.

} public function render_checkbox_field() { options = this->get_options_with_defaults() echo ltlabel> echo ltinput type=checkbox name= . esc_attr( this->option_name ) . [checkbox] value=1 . checked( 1, options[checkbox], false ) . /> echo nbspEnable this option echo lt/label> echo ltp class=description>A boolean checkbox.

} public function render_select_field() { options = this->get_options_with_defaults() echo ltselect name= . esc_attr( this->option_name ) . [select]> echo ltoption value=option1 . selected( option1, options[select], false ) . gtOption 1lt/option> echo ltoption value=option2 . selected( option2, options[select], false ) . gtOption 2lt/option> echo ltoption value=option3 . selected( option3, options[select], false ) . gtOption 3lt/option> echo lt/select> } public function render_radio_field() { options = this->get_options_with_defaults() echo ltlabel>ltinput type=radio name= . esc_attr( this->option_name ) . [radio] value=yes . checked( yes, options[radio], false ) . /> Yeslt/label>nbspnbsp echo ltlabel>ltinput type=radio name= . esc_attr( this->option_name ) . [radio] value=no . checked( no, options[radio], false ) . /> Nolt/label> } public function render_color_field() { options = this->get_options_with_defaults() echo ltinput type=text name= . esc_attr( this->option_name ) . [color] value= . esc_attr( options[color] ) . class=example-color-field /> echo ltp class=description>Use the color picker to choose a hex color.

} public function render_image_field() { options = this->get_options_with_defaults() image_id = intval( options[image_id] ) image_src = image_id ? wp_get_attachment_image_url( image_id, thumbnail ) : echo ltinput type=hidden name= . esc_attr( this->option_name ) . [image_id] class=example-image-id value= . esc_attr( image_id ) . /> echo ltimg src= . esc_attr( image_src ) . alt= class=example-image-preview style=max-width:120px display:block margin-bottom:8px /> echo ltbutton class=button example-upload-button>Upload/Choose Imagelt/button> echo ltbutton class=button example-remove-image>Removelt/button> echo ltp class=description>Select an image from the Media Library.

} // Sanitize callback: validate and sanitize input before saving public function sanitize_callback( input ) { output = array() // Ensure input is an array if ( ! is_array( input ) ) { add_settings_error( example_settings_api, invalid_input, Invalid input format., error ) return this->defaults } // text_field if ( isset( input[text_field] ) ) { output[text_field] = sanitize_text_field( input[text_field] ) if ( strlen( output[text_field] ) gt 100 ) { add_settings_error( example_settings_api, text_too_long, Text field cannot exceed 100 characters., error ) output[text_field] = substr( output[text_field], 0, 100 ) } } else { output[text_field] = this->defaults[text_field] } // textarea if ( isset( input[textarea] ) ) { output[textarea] = wp_kses_post( input[textarea] ) } else { output[textarea] = this->defaults[textarea] } // checkbox (stored as 1 or 0) output[checkbox] = isset( input[checkbox] ) intval( input[checkbox] ) === 1 ? 1 : 0 // select (validate allowed values) allowed_selects = array( option1, option2, option3 ) output[select] = in_array( input[select], allowed_selects, true ) ? input[select] : this->defaults[select] // radio allowed_radios = array( yes, no ) output[radio] = in_array( input[radio], allowed_radios, true ) ? input[radio] : this->defaults[radio] // color: validate as hex color (#rrggbb or #rgb) if ( isset( input[color] ) ) { color = trim( input[color] ) if ( preg_match( /^#([A-Fa-f0-9]{6}[A-Fa-f0-9]{3})/, color ) ) { output[color] = color } else { add_settings_error( example_settings_api, invalid_color, Invalid color value. Please provide a valid hex color., error ) output[color] = this->defaults[color] } } else { output[color] = this->defaults[color] } // image_id: ensure its an integer and attachment exists image_id = isset( input[image_id] ) ? intval( input[image_id] ) : 0 if ( image_id ) { mime = get_post_mime_type( image_id ) if ( mime strpos( mime, image/ ) === 0 ) { output[image_id] = image_id } else { add_settings_error( example_settings_api, invalid_image, Selected file is not a valid image., error ) output[image_id] = 0 } } else { output[image_id] = 0 } // After sanitization, add a success message if no settings errors if ( ! get_settings_errors() ) { add_settings_error( example_settings_api, settings_updated, Settings saved., updated ) } return output } // Options page HTML public function options_page_html() { if ( ! current_user_can( manage_options ) ) { return } // Show settings errors and messages settings_errors( example_settings_api ) echo ltdiv class=wrap> echo lth2>Example Settingslt/h2> echo ltform method=post action=options.php> // Output nonce, option_page and other hidden fields for the registered setting group settings_fields( example_settings_group ) // Output settings sections and fields do_settings_sections( example-settings-api ) // Submit button submit_button() echo lt/form> echo lt/div> } } new Example_Settings_API_Plugin() ?gt

How the code above is organized (explanation)

  1. Constructor hooks into admin_menu, admin_init, and admin_enqueue_scripts.
  2. add_admin_menu() registers the options page (add_options_page).
  3. register_settings() calls register_setting(), add_settings_section(), and add_settings_field() for each field. register_setting ties a sanitize callback to the option.
  4. Field render callbacks output the actual HTML for each field. They retrieve options with get_option() and provide sensible defaults using wp_parse_args().
  5. sanitize_callback() centralizes validation and sanitization. Use WP functions like sanitize_text_field(), wp_kses_post(), intval(), and regex validation for color values. Use add_settings_error() to provide feedback on validation problems.
  6. options_page_html() builds the HTML form using settings_fields() to output the necessary nonces and option group hidden fields, do_settings_sections() to output sections and their fields, and submit_button() to provide the Save Changes button. settings_errors() prints messages created via add_settings_error().
  7. enqueue_admin_assets() enqueues the WP color picker and Media Library scripts/CSS, and registers an inline script to initialize them only on this admin page.

How the Settings API flow works on form submit

  1. User submits the form on the options page.
  2. WordPress verifies the nonce generated by settings_fields() and user capability.
  3. WordPress calls the sanitize callback registered with register_setting(), passing the raw posted data.
  4. Sanitize callback should return the sanitized/validated data (or defaults). You can use add_settings_error() within sanitize callback to report issues.
  5. WordPress updates the option in the database (wp_options) when sanitize callback returns the value(s).
  6. On page reload settings_errors() displays any messages you added.

Accessing and using saved settings in theme or plugin code

Retrieve saved options with get_option(). The example stores one option as an array under the key example_settings_api_options. Always provide defaults and escape output for safety.

options = get_option( example_settings_api_options, array() )
options = wp_parse_args( options, array(
    text_field =gt ,
    color =gt #ffffff,
    // other defaults...
) )

// Use values safely:
echo ltdiv style=background: . esc_attr( options[color] ) . >
echo lth2> . esc_html( options[text_field] ) . lt/h2>
echo lt/div>

Common field types and their output/escape patterns

  • Text input: use esc_attr() for the value attribute.
  • Textarea: use esc_textarea() for contents.
  • Checkbox: saved as 1 or 0 use checked( 1, value ) when rendering.
  • Select/radio: validate allowed values in sanitize callback use selected()/checked() for rendering.
  • Color: store as a hex string validate via regex in sanitize callback use esc_attr() on output.
  • Image uploads: store attachment ID verify MIME and existence on sanitize get image URL with wp_get_attachment_image_url() and escape with esc_url().

Using settings_errors() and add_settings_error()

add_settings_error() allows you to add error or update notices programmatically. Pass a unique slug, error code, message, and type (error or updated). Call settings_errors( setting_slug ) on your options page to display any messages for that slug.

Best practices and tips

  • Use a single option array (recommended for many related settings). It reduces DB rows and simplifies defaults handling. But for big settings or when other plugins/themes should use separate options, separate option names are acceptable.
  • Sanitize centrally with the sanitize callback of register_setting(). Validate all fields and do not trust POSTed data.
  • Escape at output time with the appropriate escaping function. Sanitize for storage, escape for output.
  • Capability checks: Ensure only authorized users can view and save settings (manage_options or other capability appropriate to your plugin).
  • Enqueue assets conditionally: Only load scripts/styles (color picker, media uploader) on your settings page.
  • Messages: Use add_settings_error() for storing warnings/errors from sanitize callback and show them with settings_errors().
  • Defaults and migrations: Provide defaults with wp_parse_args() and handle migrations if you rename option keys in future plugin versions.
  • Nonces: settings_fields() outputs required nonces — do not try to handle them manually for options registered via Settings API.

Troubleshooting common issues

  • Fields not saving: Ensure option_group in settings_fields() matches the group used in register_setting(). Also confirm register_setting() is called on admin_init (not too late) and that your option name matches the input name attribute.
  • Nonces missing / permissions errors: Verify settings_fields() is present in your form and the user has required capability (manage_options by default).
  • Values sanitized away unexpectedly: Check your sanitize callback and ensure you are returning the final sanitized value. If your callback returns null or an empty string unintentionally, values will be removed.
  • Scripts not loading: Confirm admin_enqueue_scripts only enqueues assets on the correct hook. The hook value for options pages under Settings is settings_page_{menu_slug}.

Advanced topics (brief)

  • Repeatable fields / arrays: Store repeating fields as nested arrays and sanitize each row in the sanitize callback. Use JS to add/remove rows in the admin UI handle array keys carefully in field names (e.g., name=my_option[field][0][subfield]).
  • REST API integration: You can expose options via custom REST endpoints if needed, but avoid exposing sensitive data publicly.
  • Multisite: If building for multisite, you may need to use get_site_option() / update_site_option() for network-wide options and handle capability checks for network admins.
  • Settings sections on multiple pages: The (page) parameter of add_settings_section and add_settings_field ties fields to specific admin pages reuse sections across pages by using the same page slug.

References

Summary

The Settings API provides a structured, secure, and extensible way to add settings and options pages in WordPress. The recommended approach is to register settings and fields using register_setting(), add_settings_section(), and add_settings_field(), output the form with settings_fields() and do_settings_sections(), and sanitize input in the sanitize callback. Enqueue assets only on your page, validate and escape everything, and use add_settings_error() to communicate validation and success messages to the user.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

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