Create a WordPress plugin settings page with the Settings API

The WordPress Settings API is the official way to add plugin or theme option pages to the WordPress admin. It provides a structured system of settings groups, sections, and fields that handles sanitisation, nonce verification, and the actual wp_options database save/retrieve cycle for you — all via three registration functions: register_setting(), add_settings_section(), and add_settings_field(). The API is verbose compared to raw HTML forms, but it integrates with WordPress’s options.php POST handler (which means WordPress handles security, sanitisation callbacks, and redirects), works correctly with WordPress network sites, and ensures your settings page behaves consistently with other WordPress admin screens. The alternative — a custom HTML form posting to admin-post.php — requires you to implement all of this manually. The Settings API works with the Options API under the hood: each registered setting corresponds to an entry in wp_options. Grouping related settings into a single serialised array option (one register_setting() call) is more efficient than registering each field as a separate option. Each field gets a sanitisation callback where you sanitise and validate user input before it hits the database — pair this with the sanitisation guide for the full pattern.

Problem: You need a WordPress plugin settings page in the admin dashboard with form fields that save securely to the database without writing raw form-handling code.

Solution: Add the following code to your plugin’s main file:

<?php
// Add admin menu page
add_action( 'admin_menu', 'helloadmin_settings_menu' );
function helloadmin_settings_menu(): void {
    add_options_page(
        __( 'HelloAdmin Settings', 'helloadmin' ),
        __( 'HelloAdmin', 'helloadmin' ),
        'manage_options',
        'helloadmin-settings',
        'helloadmin_settings_page'
    );
}

// Register settings, sections, and fields
add_action( 'admin_init', 'helloadmin_register_settings' );
function helloadmin_register_settings(): void {
    // Register a single option that stores all settings as an array
    register_setting(
        'helloadmin_options_group',    // option group (used in settings_fields())
        'helloadmin_options',          // option name in wp_options
        [
            'sanitize_callback' => 'helloadmin_sanitize_options',
            'default'           => [
                'api_key'          => '',
                'posts_per_page'   => 10,
                'enable_analytics' => false,
            ],
        ]
    );

    // Section
    add_settings_section(
        'helloadmin_general_section',
        __( 'General Settings', 'helloadmin' ),
        fn() => print( '<p>' . esc_html__( 'Configure the plugin options below.', 'helloadmin' ) . '</p>' ),
        'helloadmin-settings'
    );

    // Field: API key
    add_settings_field(
        'helloadmin_api_key',
        __( 'API Key', 'helloadmin' ),
        'helloadmin_api_key_field',
        'helloadmin-settings',
        'helloadmin_general_section'
    );

    // Field: Posts per page
    add_settings_field(
        'helloadmin_posts_per_page',
        __( 'Posts per page', 'helloadmin' ),
        'helloadmin_posts_per_page_field',
        'helloadmin-settings',
        'helloadmin_general_section'
    );
}

// Field render callbacks
function helloadmin_api_key_field(): void {
    $opts = get_option( 'helloadmin_options', [] );
    $val  = $opts['api_key'] ?? '';
    printf(
        '<input type="text" name="helloadmin_options[api_key]" value="%s" class="regular-text">',
        esc_attr( $val )
    );
}

function helloadmin_posts_per_page_field(): void {
    $opts = get_option( 'helloadmin_options', [] );
    $val  = $opts['posts_per_page'] ?? 10;
    printf(
        '<input type="number" name="helloadmin_options[posts_per_page]" value="%d" min="1" max="100">',
        absint( $val )
    );
}

// Sanitise all options in one callback
function helloadmin_sanitize_options( mixed $input ): array {
    $clean = [];
    $clean['api_key']          = sanitize_text_field( $input['api_key'] ?? '' );
    $clean['posts_per_page']   = absint( $input['posts_per_page'] ?? 10 );
    $clean['enable_analytics'] = ! empty( $input['enable_analytics'] );
    return $clean;
}

// Render the settings page
function helloadmin_settings_page(): void {
    if ( ! current_user_can( 'manage_options' ) ) {
        return;
    }
    ?>
    <div class="wrap">
        <h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
        <form method="post" action="options.php">
            <?php
            settings_fields( 'helloadmin_options_group' );
            do_settings_sections( 'helloadmin-settings' );
            submit_button();
            ?>
        </form>
    </div>
    <?php
}

NOTE: Always point your form’s action to options.php, not to your own handler — this is where WordPress processes Settings API form submissions and handles the nonce verification automatically via settings_fields(). Do not add your own nonce field; settings_fields() already does so. If you need a settings page that does not use options.php (e.g. for a network admin page in multisite, or for settings that should not be autoloaded), you must handle the form POST yourself using admin_post_{action} and manage nonces manually.