Settings Page Generator
Menu registration, the Settings API wiring, a real sanitiser per field type, and the screen it all renders into — laid out the way core expects.
// Generated with WP CodeKit — powered by GrowQuest (https://growquest.io). /** * Default values for every field. * * @return array */ function acme_default_options() { return array( 'enabled' => true, 'mode' => 'fast', 'accent' => '#2271b1', 'api_key' => '', 'cache_ttl' => 3600, ); } /** * Saved options, merged over the defaults so no key is ever missing. * * @return array */ function acme_get_options() { return wp_parse_args( (array) get_option( 'acme_toolkit_options', array() ), acme_default_options() ); } /** * Add the page to the admin menu. */ function acme_add_settings_page() { $hook = add_menu_page( __( 'Acme Toolkit Settings', 'acme-toolkit' ), __( 'Acme Toolkit', 'acme-toolkit' ), 'manage_options', 'acme-toolkit', 'acme_render_settings_page', 'dashicons-admin-generic', 80 ); acme_settings_hook( $hook ); } add_action( 'admin_menu', 'acme_add_settings_page' ); /** * Remember the screen hook suffix add_menu_page() handed back. * * @param string|null $hook Hook suffix to store, or null to read it. * @return string */ function acme_settings_hook( $hook = null ) { static $stored = ''; if ( null !== $hook ) { $stored = (string) $hook; } return $stored; } /** * Register the setting, its sections and its fields. */ function acme_register_settings() { register_setting( 'acme_toolkit_group', 'acme_toolkit_options', array( 'type' => 'array', 'sanitize_callback' => 'acme_sanitize_options', 'default' => acme_default_options(), 'show_in_rest' => false, ) ); add_settings_section( 'acme_section_general', __( 'General', 'acme-toolkit' ), 'acme_render_section', 'acme-toolkit-general' ); add_settings_field( 'enabled', __( 'Enable the toolkit', 'acme-toolkit' ), 'acme_render_field', 'acme-toolkit-general', 'acme_section_general', array( 'key' => 'enabled', 'type' => 'checkbox', 'label_for' => 'enabled', 'default' => true, 'placeholder' => '', 'description' => __( 'Turn everything off without deactivating the plugin.', 'acme-toolkit' ), 'choices' => array(), ) ); add_settings_field( 'mode', __( 'Mode', 'acme-toolkit' ), 'acme_render_field', 'acme-toolkit-general', 'acme_section_general', array( 'key' => 'mode', 'type' => 'select', 'label_for' => 'mode', 'default' => 'fast', 'placeholder' => '', 'description' => __( 'Fast skips the extra validation pass.', 'acme-toolkit' ), 'choices' => array( 'fast' => __( 'Fast', 'acme-toolkit' ), 'safe' => __( 'Safe', 'acme-toolkit' ), ), ) ); add_settings_field( 'accent', __( 'Accent colour', 'acme-toolkit' ), 'acme_render_field', 'acme-toolkit-general', 'acme_section_general', array( 'key' => 'accent', 'type' => 'color', 'label_for' => 'accent', 'default' => '#2271b1', 'placeholder' => '#2271b1', 'description' => '', 'choices' => array(), ) ); add_settings_section( 'acme_section_api', __( 'API', 'acme-toolkit' ), 'acme_render_section', 'acme-toolkit-api' ); add_settings_field( 'api_key', __( 'API key', 'acme-toolkit' ), 'acme_render_field', 'acme-toolkit-api', 'acme_section_api', array( 'key' => 'api_key', 'type' => 'text', 'label_for' => 'api_key', 'default' => '', 'placeholder' => 'sk_live_…', 'description' => __( 'Found under Account → Developers.', 'acme-toolkit' ), 'choices' => array(), ) ); add_settings_field( 'cache_ttl', __( 'Cache lifetime', 'acme-toolkit' ), 'acme_render_field', 'acme-toolkit-api', 'acme_section_api', array( 'key' => 'cache_ttl', 'type' => 'number', 'label_for' => 'cache_ttl', 'default' => 3600, 'placeholder' => '3600', 'description' => __( 'Seconds to keep API responses.', 'acme-toolkit' ), 'choices' => array(), ) ); } add_action( 'admin_init', 'acme_register_settings' ); /** * Sanitise the submitted values. Anything not listed here never reaches the database. * * @param mixed $input Raw value from the form. * @return array */ function acme_sanitize_options( $input ) { $input = (array) $input; $output = acme_default_options(); $output['enabled'] = ! empty( $input['enabled'] ); $output['mode'] = isset( $input['mode'] ) && in_array( $input['mode'], array( 'fast', 'safe' ), true ) ? sanitize_key( $input['mode'] ) : 'fast'; $output['accent'] = isset( $input['accent'] ) && sanitize_hex_color( $input['accent'] ) ? sanitize_hex_color( $input['accent'] ) : '#2271b1'; $output['api_key'] = isset( $input['api_key'] ) ? sanitize_text_field( $input['api_key'] ) : ''; $output['cache_ttl'] = isset( $input['cache_ttl'] ) ? absint( $input['cache_ttl'] ) : 3600; return $output; } /** * Print the blurb under a section heading. * * @param array $args Section arguments from add_settings_section(). */ function acme_render_section( $args ) { $descriptions = array( 'acme_section_general' => __( 'How the toolkit behaves on the front end.', 'acme-toolkit' ), 'acme_section_api' => __( 'Credentials for the remote service.', 'acme-toolkit' ), ); if ( ! empty( $descriptions[ $args['id'] ] ) ) { echo '<p>' . esc_html( $descriptions[ $args['id'] ] ) . '</p>'; } } /** * Print one field. Every type routes through here so escaping lives in one place. * * @param array $args Field arguments from add_settings_field(). */ function acme_render_field( $args ) { $options = acme_get_options(); $key = $args['key']; $name = 'acme_toolkit_options[' . $key . ']'; $value = isset( $options[ $key ] ) ? $options[ $key ] : $args['default']; switch ( $args['type'] ) { case 'checkbox': printf( '<input type="checkbox" id="%1$s" name="%2$s" value="1"%3$s />', esc_attr( $key ), esc_attr( $name ), checked( (bool) $value, true, false ) ); break; case 'select': echo '<select id="' . esc_attr( $key ) . '" name="' . esc_attr( $name ) . '">'; foreach ( $args['choices'] as $choice => $label ) { printf( '<option value="%1$s"%2$s>%3$s</option>', esc_attr( $choice ), selected( $value, $choice, false ), esc_html( $label ) ); } echo '</select>'; break; default: printf( '<input type="%1$s" id="%2$s" name="%3$s" value="%4$s" class="regular-text" placeholder="%5$s" />', esc_attr( $args['type'] ), esc_attr( $key ), esc_attr( $name ), esc_attr( $value ), esc_attr( $args['placeholder'] ) ); break; } if ( ! empty( $args['description'] ) ) { echo '<p class="description">' . esc_html( $args['description'] ) . '</p>'; } } /** * Render the settings screen. */ function acme_render_settings_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } $tabs = array( 'general' => __( 'General', 'acme-toolkit' ), 'api' => __( 'API', 'acme-toolkit' ), ); $active_tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! isset( $tabs[ $active_tab ] ) ) { $active_tab = 'general'; } ?> <div class="wrap"> <h1><?php echo esc_html( get_admin_page_title() ); ?></h1> <?php settings_errors( 'acme_toolkit_options' ); ?> <h2 class="nav-tab-wrapper"> <?php foreach ( $tabs as $tab_key => $tab_label ) : ?> <a href="<?php echo esc_url( add_query_arg( array( 'page' => 'acme-toolkit', 'tab' => $tab_key ), admin_url( 'admin.php' ) ) ); ?>" class="nav-tab <?php echo $active_tab === $tab_key ? 'nav-tab-active' : ''; ?>" > <?php echo esc_html( $tab_label ); ?> </a> <?php endforeach; ?> </h2> <form action="options.php" method="post"> <?php settings_fields( 'acme_toolkit_group' ); do_settings_sections( 'acme-toolkit-' . $active_tab ); submit_button(); ?> </form> </div> <?php } /** * Load assets on this screen only — never on every admin page. * * @param string $hook_suffix Current admin screen. */ function acme_admin_assets( $hook_suffix ) { if ( acme_settings_hook() !== $hook_suffix ) { return; } wp_enqueue_style( 'acme-toolkit-admin', plugins_url( 'assets/admin.css', __FILE__ ), array(), '1.0.0' ); } add_action( 'admin_enqueue_scripts', 'acme_admin_assets' ); /* * uninstall.php — sits next to the plugin file and runs when the plugin is deleted. * * defined( 'WP_UNINSTALL_PLUGIN' ) || exit; * delete_option( 'acme_toolkit_options' ); */
About this generator
Settings Page Generator Online
This WordPress settings page generator writes the whole admin screen for you: the add_menu_page() or add_submenu_page() call, the register_setting() / add_settings_section() / add_settings_field() trio that has to line up exactly, a real sanitise callback, and the render functions that print every field type with the right escaping. Nine field types are supported — text, textarea, number, checkbox, select, radio, email, URL and colour.
The Preview tab redraws the actual admin screen as you type, tabs and all, so you can see where a section lands before you paste anything into a site. Output as a bare snippet, a functions.php block or a standalone plugin file, in procedural functions or a single final class. Free, no account, and nothing you type leaves the browser.
Reviewed Output tested on WordPress 6.8 · PHP 8.3
Why this WordPress Settings API generator beats a hand-written options page
The Settings API is not hard, it is fiddly. Four separate calls have to agree on the same option group, the same page slug and the same section id, and when one of them disagrees the page renders with no fields and no error message. Add the fact that a top-level menu never prints "Settings saved." on its own, and most hand-written options screens ship half-broken. This generator wires the pieces together and then audits the result.
The four calls actually agree
register_setting(), add_settings_section(), add_settings_field(), settings_fields() and do_settings_sections() are all emitted from one derived slug, so the group, page and section ids can never drift apart.
A sanitise callback that names every key
Array storage gets a generated sanitize_options() that starts from the defaults and copies across only the keys you declared — checkboxes through ! empty(), selects and radios checked against their own choice list with in_array(), colours through sanitize_hex_color(). Anything not listed never reaches the database.
The saved notice that top-level pages forget
Submenu pages under Settings print "Settings saved." for free; a top-level menu does not. Leave settings_errors() off on a top-level page and the generator raises a warning with a one-click fix, because a save that looks like it did nothing is the most-reported bug on custom options screens.
One option row or many, with the cost stated
Store every field in a single array option (one row, one register_setting(), one sanitiser) or one option per field with a core sanitise callback each. Pick the second with more than three fields and the checker points out you have just added that many autoloaded rows to wp_options.
Assets scoped to your screen only
Turn on screen-scoped assets and the generator keeps the hook suffix add_menu_page() returned — in a class property, or a static in a small helper function for the procedural build — and compares it inside admin_enqueue_scripts so your CSS never loads on someone else's page.
Fifteen checks before you paste
Uppercase or spaced menu slugs, a read capability on a settings screen, duplicate field keys, fields pointing at a deleted section, select fields with no choices, an unprefixed option name, tabs with only one section, a text domain that does not match the slug. Most come with a one-click fix.
How does the Settings Page Generator work?
Four steps. Describe the menu entry, decide where the values live, add sections and fields, then clear the checks and export.
- 01
Describe the page
Page title, menu title, menu slug, and whether this is a top-level menu or a submenu under Settings, Tools, Appearance, Users or a parent file of your own. Pick the capability and, for a top-level menu, a Dashicon and a menu position — the generator explains what each position band lands next to.
- 02
Choose storage and naming
Set the function prefix, text domain, option name and option group, then choose one array option or one option per field. Procedural functions or a single final class — the same code either way, just wrapped differently.
- 03
Add sections and fields
Create the sections that group the screen, then add fields and assign each one to a section. Every field carries a type, default, placeholder, description and — for selects and radios — a comma-separated
value:Labelchoice list. - 04
Clear the checks and export
Work through the Checks tab, apply the one-click fixes, then optionally add tabs, a reset-to-defaults button, REST exposure with a generated object schema, and the
uninstall.phpsnippet that deletes the option. Copy the snippet or download the.phpfile.
Worked example — a top-level Acme Toolkit menu at position 80
The menu registration exactly as the generator emits it. add_menu_page() returns a hook suffix, which the screen-scoped assets option captures for you.
/**
* Add the page to the admin menu.
*/
function acme_add_settings_page() {
add_menu_page(
__( 'Acme Toolkit Settings', 'acme-toolkit' ),
__( 'Acme Toolkit', 'acme-toolkit' ),
'manage_options',
'acme-toolkit',
'acme_render_settings_page',
'dashicons-admin-generic',
80
);
}
add_action( 'admin_menu', 'acme_add_settings_page' );The rest of the generated file is the part people get wrong: acme_register_settings() on admin_init calls register_setting( 'acme_toolkit_group', 'acme_toolkit_options', array( 'sanitize_callback' => 'acme_sanitize_options', … ) ), then add_settings_section() and add_settings_field() against the same acme-toolkit page slug. Miss the sanitize_callback and WordPress stores whatever was posted, unfiltered.
WordPress settings pages — frequently asked questions
The questions that come up most often when building an options screen with the Settings API.
Why does my settings page say "Options page not found" when I save?
The option group passed to settings_fields() in the form does not match the first argument of register_setting(). options.php looks up the group by name and rejects anything it does not recognise. Both values come from one field in this generator, so they cannot drift; if you are debugging existing code, compare the two strings character by character.
Why do my fields not appear on the page at all?
The page slug in do_settings_sections() must match the fourth argument of add_settings_section() and add_settings_field() exactly. It is a free-form string, not the menu slug, and WordPress prints nothing rather than warning you when it does not match. Turning tabs on changes that slug to {slug}-{section}, which is another common way to lose the fields.
Why does my top-level settings page not show "Settings saved."?
Core only prints that notice automatically on pages under Settings. On a top-level menu you have to call settings_errors() yourself inside the render callback, and the redirect back adds settings-updated=true to the URL. The generator emits the call and flags a warning if you switch it off on a top-level page.
Should I use one option for everything or one option per field?
One array option is usually right: a single row in wp_options, one register_setting() call, one sanitise callback, one row to delete on uninstall. One option per field is easier to read with get_option() from elsewhere, but every one of them is autoloaded on every page load of the site, front end included. Above three or four fields the array wins.
Do I need a sanitize_callback if I am already escaping on output?
Yes. Without sanitize_callback WordPress writes the posted value to the database as it arrived. Escaping on output protects that one template, not the next plugin that reads the option, the REST response, or an export. The generated sanitiser also rebuilds the array from your declared keys, so a crafted POST cannot inject extra ones.
What menu position should I use for add_menu_page()?
Positions 80 to 100 sit below the content menus and around Settings, which is where plugin screens are conventionally expected. Below 5 puts you above Posts, and 5 to 25 drops you between the core content menus. Positions are not reserved, so two plugins using the same integer will fight — WordPress resolves it by nudging one of them, which is why the same number can behave differently on two sites.
Where do I paste the code from the Settings Page generator?
You have three good options and one bad one. Best is a small site-specific plugin — the generator can output one for you, and it keeps working when you change theme. A code-snippets plugin is fine too. A child theme's functions.php works but ties the code to that theme. Never edit the parent theme: the next update overwrites it and your code is gone.
Can I use the generated code in client work or a commercial plugin?
Yes. The output is ordinary WordPress API calls that you configured — there is no licence attached to it and no attribution required. Use it in client sites, commercial plugins and themes, or products you sell. The generated file carries a short credit comment you are free to delete.
The other Admin generators in the library, plus the WordPress tools most often used alongside this one.