Settings pages are one of the first things a plugin developer builds and one of the most frequently done badly. I’ve reviewed plenty of plugins where the settings page is a hand-written form that posts to itself, reads $_POST directly, saves values without sanitising them and skips nonce checks. It works, until someone finds a way to inject a script through an unescaped field or trick an administrator into changing settings with a forged request. The WordPress Settings API exists so you never have to write that plumbing yourself.
WordPress has had a proper system for this since version 2.7: the Settings API. It handles form submission, nonce verification, capability checks and saving to the options table. You describe your settings, sections and fields, and WordPress does the plumbing.
The Settings API has a reputation for being verbose, and it is a little. But once you’ve built one page with it, the pattern is easy to reuse, and you get security and consistency for free.
The pieces
The Settings API has four main concepts:
- Settings: the options stored in the database, registered with register_setting.
- Sections: groups of fields on a page, registered with add_settings_section.
- Fields: individual inputs, registered with add_settings_field.
- The page: an admin menu page that renders everything, added with add_options_page or add_menu_page.
The form on the page posts to options.php, a core file that validates and saves registered settings. You never write the save logic yourself.
Step 1: Add the menu page
For most plugins, a page under the Settings menu is appropriate:
The arguments are the page title, the menu label, the capability required, a slug and the function that renders the page. The manage_options capability limits access to administrators.
Step 2: Register the setting
Store all your plugin’s settings in a single option as an array, rather than many separate options. It’s tidier, loads in one query and is easier to delete on uninstall.
The first argument is the option group, which ties the form to this setting. The second is the option name in the database. The sanitize callback runs whenever the option is saved.
Step 3: Add sections and fields
Still inside admin_init:
The label_for argument makes WordPress wrap the field title in a label element connected to your input’s ID, which matters for accessibility.
Step 4: Render the fields
Each field callback outputs its input. Read the current value with get_option, and always escape output:
Note the input names: acme_notices_options[enabled]. That’s how values end up inside the single option array.
Step 5: Render the page
The page callback outputs the form:
settings_fields outputs hidden fields, including the nonce and option group. do_settings_sections prints every section and field registered for the page. submit_button adds a standard save button.
Step 6: Sanitize properly
The sanitize callback is where security happens. It receives the submitted values and must return clean data. Never trust input, even from administrators.
Each field is sanitised according to what it should contain. Checkboxes become 0 or 1. Text with limited HTML goes through wp_kses with a strict allowed list. Select values are checked against an allow list. Invalid combinations generate a user-friendly error.
Use the right sanitising function for each type: sanitize_text_field for plain text, sanitize_email for emails, esc_url_raw for URLs, absint for positive integers.
Showing errors and success messages
On pages under the Settings menu, WordPress shows settings errors and the “Settings saved” message automatically. On top-level menu pages you add yourself, you need to call settings_errors() in your page output to display them.
Autoloading and performance
Options are autoloaded by default, meaning WordPress loads them on every page request. For settings needed on every front-end page, that’s appropriate. For large settings arrays only used in the admin, it wastes memory.
Since WordPress 6.6, you can control autoloading more precisely when adding or updating options, and WordPress warns in Site Health when autoloaded options become very large. Keep settings arrays small, and avoid storing large data, such as cached API responses, in autoloaded options.
Adding contextual help
A short help tab explains settings without cluttering the page:
Help tabs are underused. They’re a good place for explanations that would otherwise end up in support emails.
Cleaning up on uninstall
When someone deletes your plugin, remove its options. Add an uninstall.php file in the plugin root:
Leaving orphaned options in every database your plugin ever touched is poor manners.
When to go beyond the Settings API
For complex interfaces, such as multi-step setup wizards, drag-and-drop builders or settings with live previews, many developers now build React-based admin pages using WordPress components and save settings through the REST API, with register_setting’s show_in_rest option. That approach gives a more modern interface but requires a build process and more code.
For the majority of plugins, which need a handful of checkboxes, text fields and selects, the classic Settings API is still the quickest, most secure and most consistent option. It looks like the rest of the WordPress admin, which users appreciate, and it handles the security details you’d otherwise have to remember yourself.
Why a single option array matters more than it seems
It’s tempting to register one option per field: acme_enabled, acme_message, acme_position and so on. On a small plugin, that seems harmless. But over time, plugins grow. Ten options become forty. Each one is a separate row in wp_options, and if they’re autoloaded, each one is loaded into memory on every request, whether the plugin needs it or not.
Worse, separate options make uninstalling messy and migrations awkward. If you ever need to rename a setting or change its format, a single array lets you do it in one place with a simple version check.
A pattern I use is to store a schema version inside the array itself, so upgrades can be handled cleanly:
That runs once after an update and quietly moves old data into the new structure.
Accessibility of settings pages
Settings pages are used by people with all kinds of abilities, and the WordPress admin aims to be accessible. A few details help:
- Always connect labels to inputs, using label_for or explicit label elements.
- Use fieldset and legend for groups of radio buttons or checkboxes, so screen readers announce the group’s purpose.
- Use the description paragraph for help text and connect it with aria-describedby when the help is essential to completing the field.
- Don’t rely on colour alone to show errors. The Settings API’s error notices include text, which is good.
Using standard admin markup and classes, such as form-table and regular-text, keeps your page consistent with the rest of the admin, which also benefits keyboard and screen reader users who know how core pages work.
Testing your settings page
Before releasing, test a few scenarios that commonly break:
- Save the form with every field empty and confirm the defaults behave sensibly.
- Enter unexpected input, like HTML in a text field or an invalid value in a select using browser developer tools, and confirm the sanitize callback handles it.
- Log in as an editor and confirm they can’t reach the page or save the option.
- Save, reload and check every value persisted correctly, especially checkboxes, which send nothing when unticked.
- Run the plugin on a multisite network if you support it, and decide whether settings are per site or network-wide.
The unticked checkbox case catches many developers. Because browsers don’t submit unchecked checkboxes, your sanitize callback must treat a missing key as “off”, as in the example above, rather than leaving the old value in place.
Network-wide settings on multisite
The Settings API as described works per site. For multisite plugins that need network-wide settings, you’ll use a network admin menu page and handle saving through the network_admin_edit_ action, because options.php only saves site options. Store network settings with update_site_option rather than update_option. It’s a little more work, so decide early whether your plugin needs network settings at all.
Linking to the settings page
Add a “Settings” link on the Plugins screen using the plugin_action_links filter, so users can find the page straight after activation. It’s a small touch that saves many support questions about where the options live.
Keep labels short and clear
Field labels should say what the setting does in a few words. Put longer explanations in the description below the field, not in the label itself. Learn the WordPress Settings API once and every future plugin settings page becomes a quick, familiar job.
Frequently asked questions
Why use one option array instead of separate options?
Fewer database rows, one query to load everything, simpler cleanup and less risk of name collisions with other plugins.
Do I need to add a nonce myself?
No. settings_fields outputs the nonce, and options.php verifies it. That’s one of the main benefits of the Settings API.
Can non-administrators save settings?
By default, options.php requires manage_options. To let another role save your settings, use the option_page_capability_{group} filter to change the required capability.
How do I add tabs to a settings page?
Register separate option groups and sections for each tab, and render only the active tab’s group based on a query parameter. Each tab’s form saves its own group.




