Tab Navigation in WordPress — Organizing Options Pages
As your theme or plugin grows, the number of settings inevitably increases. Three sections on one page — tolerable. Ten sections — already unreadable. Users are forced to scroll endlessly looking for the right field, losing context and time. The solution familiar to every desktop application user is tab navigation.
WordPress provides ready-made CSS infrastructure for creating tabs in the admin panel. The nav-tab-wrapper and nav-tab classes style navigation exactly like on the Themes or Plugins pages. You only need to add tab switching logic and Settings API option group bindings. In this article, we will implement a full multi-tab settings panel with two tabs, active tab state preservation, and success message display via settings_errors().
Implementation Plan
Our goal is to create an options page with two tabs: Display Options and Social Networks. The first tab is active on page load. After saving settings, the active tab does not reset — the user stays where they were before clicking the Save button. On successful save, a standard WordPress notification is displayed.
Technical plan:
- Create HTML tab structure with nav-tab-wrapper and nav-tab classes
- Add a
tabquery parameter to identify the active tab - Check the
tabparameter in the options page callback and apply nav-tab-active to the active tab - Render the corresponding option group content based on the active tab
- Add a hidden field to the form so WordPress remembers the active tab on save
- Call
settings_errors()to display success/error messages
HTML Tab Structure
WordPress uses two CSS classes for tab styling: nav-tab-wrapper — a container grouping tabs into a row, and nav-tab — styling for an individual tab. The active tab additionally receives the nav-tab-active class. The HTML structure looks like this:
Sandbox Theme Options
Display Options Social Networks
Note: tabs are <a> links, not buttons or list items. WordPress styles exactly this structure, and deviating from it will cause tabs to lose the native admin panel appearance.
Determining the Active Tab via Query Parameter
Each tab links to the same options page but with a different tab parameter value: admin.php?page=sandbox-theme-options&tab=display. The page callback checks this parameter and determines which tab to highlight and which option group content to show:
function sandbox_theme_options_page_html() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$active_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'display';
?>
sanitize_key() function is critically important for security. The $_GET['tab'] parameter comes from the user and may contain XSS vectors. sanitize_key() keeps only letters, digits, hyphens, and underscores — a safe character subset.Registering Sections for Different Pages
The attentive reader will notice that do_settings_sections() is called with different page slugs depending on the active tab. This means sections must be registered for different pages. Here is the updated registration function:
function sandbox_register_settings() {
register_setting(
'sandbox_options_group',
'sandbox_theme_options',
array(
'type' => 'array',
'sanitize_callback' => 'sandbox_sanitize_options',
'default' => array(
'show_header' => 1, 'show_sidebar' => 1, 'show_footer' => 1,
'facebook_url' => '', 'twitter_url' => '',
),
)
);
add_settings_section(
'sandbox_display_section', 'Display Options',
'sandbox_display_section_callback', 'sandbox_display_options_page'
);
add_settings_section(
'sandbox_social_section', 'Social Network Links',
'sandbox_social_section_callback', 'sandbox_social_options_page'
);
}
add_action( 'admin_init', 'sandbox_register_settings' );[/codeblock]The key point: all fields still belong to a single option group (sandbox_options_group) and are stored in a single array (sandbox_theme_options). Separation happens only at the interface level — different sections are registered for different page slugs. When saving, WordPress updates the entire options array regardless of which tab the Save button was clicked on.
Preserving Tab State After Form Submission
When the Save button is clicked, WordPress redirects to a URL without the tab query parameter. As a result, the active tab resets to the default — the user always returns to the first tab after saving. This is inconvenient. To fix it, add a hidden field to the form and handle it on redirect:
echo '';[/codeblock]Now add code to preserve the tab during redirects. WordPress fires the wp_redirect hook when redirecting after settings save. We intercept it and add the tab parameter:
function sandbox_preserve_active_tab( $location ) {
if ( isset( $_POST['sandbox_active_tab'] ) ) {
$location = add_query_arg( 'tab', sanitize_key( $_POST['sandbox_active_tab'] ), $location );
}
return $location;
}
add_filter( 'wp_redirect', 'sandbox_preserve_active_tab' );[/codeblock]settings_errors() — Success and Error Messages
The settings_errors() function displays standard WordPress notifications about successful saves, validation errors, or warnings. The call must be placed before the tab navigation so the message is always visible:
settings_errors( 'sandbox_theme_options' );[/codeblock]WordPress automatically adds a "Settings saved." message on successful save via the Settings API. Custom messages are added via add_settings_error() in the sanitization function:
function sandbox_sanitize_options( $input ) {
$sanitized = array();
if ( isset( $input['facebook_url'] ) && ! empty( $input['facebook_url'] ) ) {
$sanitized['facebook_url'] = esc_url_raw( $input['facebook_url'] );
$parsed = wp_parse_url( $sanitized['facebook_url'] );
if ( ! isset( $parsed['host'] ) || strpos( $parsed['host'], 'facebook.com' ) === false ) {
add_settings_error(
'sandbox_theme_options',
'invalid_facebook_url',
'Facebook URL must point to facebook.com.',
'error'
);
$sanitized['facebook_url'] = '';
}
}
return $sanitized;
}[/codeblock]Handling Missing Tab Parameter
On the first visit to the options page, the tab parameter is absent from the URL. Our code must handle this gracefully and show the default tab:
$active_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'display';[/codeblock]If no query parameter is passed, $active_tab gets the value 'display', and the page shows the first tab. This is the standard behavior users expect.
Complete Tab Navigation Code
Here is the complete functions.php with tab navigation, two tabs, active tab state preservation, and settings_errors() messages:
'array', 'sanitize_callback' => 'sandbox_sanitize_options',
'default' => array(
'show_header' => 1, 'show_sidebar' => 1, 'show_footer' => 1,
'facebook_url' => '', 'twitter_url' => '',
),
)
);
add_settings_section(
'sandbox_display_section', 'Display Options',
'sandbox_display_section_callback', 'sandbox_display_options_page'
);
$display = array( 'show_header' => 'Show Header', 'show_sidebar' => 'Show Sidebar', 'show_footer' => 'Show Footer' );
foreach ( $display as $k => $l ) {
add_settings_field(
'sandbox_' . $k, $l, 'sandbox_checkbox_field_callback',
'sandbox_display_options_page', 'sandbox_display_section',
array( 'label_for' => 'sandbox_' . $k, 'option' => 'sandbox_theme_options', 'key' => $k )
);
}
add_settings_section(
'sandbox_social_section', 'Social Network Links',
'sandbox_social_section_callback', 'sandbox_social_options_page'
);
$social = array( 'facebook_url' => 'Facebook URL', 'twitter_url' => 'Twitter URL' );
foreach ( $social as $k => $l ) {
add_settings_field(
'sandbox_' . $k, $l, 'sandbox_url_field_callback',
'sandbox_social_options_page', 'sandbox_social_section',
array( 'label_for' => 'sandbox_' . $k, 'option' => 'sandbox_theme_options', 'key' => $k )
);
}
}
add_action( 'admin_init', 'sandbox_register_settings' );
function sandbox_display_section_callback() { echo 'Toggle visibility of page containers.
'; } function sandbox_social_section_callback() { echo 'Enter your social media profile URLs.
'; } function sandbox_checkbox_field_callback( $a ) { $o = get_option( $a['option'], array() ); $c = isset( $o[ $a['key'] ] ) ? (bool) $o[ $a['key'] ] : false; echo ''; } function sandbox_url_field_callback( $a ) { $o = get_option( $a['option'], array() ); $v = isset( $o[ $a['key'] ] ) ? $o[ $a['key'] ] : ''; echo ''; } function sandbox_sanitize_options( $input ) { $out = array(); foreach ( array( 'show_header', 'show_sidebar', 'show_footer' ) as $f ) { $out[ $f ] = isset( $input[ $f ] ) && $input[ $f ] ? 1 : 0; } foreach ( array( 'facebook_url', 'twitter_url' ) as $f ) { $out[ $f ] = isset( $input[ $f ] ) && ! empty( $input[ $f ] ) ? esc_url_raw( $input[ $f ] ) : ''; } return $out; } function sandbox_theme_options_page_html() { if ( ! current_user_can( 'manage_options' ) ) { return; } $active_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'display'; ?> Testing Tab NavigationAfter adding the code to functions.php, verify the behavior:
- Visit Appearance → Theme Options. The Display Options tab should be active
- Switch to Social Networks — the URL should include
tab=social - Enter a Facebook URL and click Save — the tab should not reset to Display
- Enter an invalid URL and save — an error message should appear above the tabs
If the tab resets after saving, check that the wp_redirect filter is added and the sandbox_active_tab hidden field is present in the form. If messages do not appear, check the settings_errors() call and the slug matching the first parameter of add_settings_error().
WordPress Notification Functions Comparison
WordPress provides several functions for displaying admin notices, and understanding the differences is important:
| Function | Purpose | Context | Output Location |
|---|---|---|---|
| settings_errors() | Settings API messages: save success, validation errors | Options page | At the call location on the page |
| add_settings_error() | Registers a message for settings_errors() | sanitize_callback | Where settings_errors() is called |
| admin_notices | Hook for displaying any admin notices | Any admin page | Top of the page |
| wp_die() | Critical error, stops execution | Any context | Separate error page |
For options pages, the correct combination is add_settings_error() in the sanitizer and settings_errors() in the page callback. This gives native-looking notifications and correctly handles all states: success, warning, error, and informational messages.
Expanding the Tab System
The current implementation supports two tabs. Adding a third requires:
- A new element in nav-tab-wrapper: a link with nav-tab class and nav-tab-active condition
- New sections and fields registered for a new page (e.g., 'sandbox_general_options_page')
- A new elseif condition in the page callback
- A new valid value in the allowed tabs whitelist
For more than three tabs, replace the if-elseif chain with an allowed tabs array and in_array() check to prevent errors when users manually change the tab parameter to a non-existent value:
$allowed_tabs = array( 'display', 'social', 'general', 'advanced' );
$active_tab = isset( $_GET['tab'] ) && in_array( $_GET['tab'], $allowed_tabs, true )
? sanitize_key( $_GET['tab'] )
: 'display';[/codeblock]Tabs vs Separate Pages: When to Use Which
When should you use tabs versus separate pages with submenus? The choice depends on the number of settings and their logical cohesion:
| Criteria | Tabs | Separate Submenus |
|---|---|---|
| Number of settings | 10-30 fields across 2-4 tabs | 30+ fields, each page standalone |
| Data cohesion | All settings belong to one theme/plugin | Different modules with independent settings |
| Usage frequency | User frequently switches between tabs | Each page configured rarely and independently |
| Learnability | Higher — all settings on one page | Lower — need to find the right submenu |
| Menu depth | 1 menu item, no submenus | 1 item + N submenus (clutters sidebar) |
For most themes and medium plugins, tabs are optimal: one entry point via Appearance or a custom item, all settings on one page, instant switching between groups. Separate submenus are justified for large plugins like WooCommerce, where each section (Products, Orders, Shipping) is essentially a separate application.
\u{201c}Tab navigation is not a technical necessity — it is a matter of respecting the user. Two tabs with five fields each are more convenient than one page with ten fields requiring scrolling. Spend 20 minutes implementing tabs — users will thank you with a silent absence of complaints.
Frequently Asked Questions About Tab Navigation
How do I create tabs in the WordPress admin panel?
Use the HTML structure with CSS classes nav-tab-wrapper (container) and nav-tab (individual tab). The active tab gets the additional nav-tab-active class. Switching is implemented via a tab query parameter checked in the options page callback.
Why does the tab reset after saving settings?
WordPress redirects to a URL without the tab parameter by default. To preserve the tab, add a hidden field named sandbox_active_tab and a wp_redirect filter that appends the tab parameter to the redirect URL.
Is sanitize_key() required for the tab parameter?
Yes. The $_GET['tab'] parameter comes from the user and may contain XSS vectors. sanitize_key() keeps only safe characters: letters, digits, hyphens, and underscores. Without it, you risk an XSS vulnerability.
How does settings_errors() work with tabs?
settings_errors() displays WordPress notifications about settings saves. The call must be before the tab structure so the message is visible on any tab. It accepts an optional $setting parameter to filter messages by option slug.
Can different tabs have different option groups?
No, a single form supports only one option group (one settings_fields() call). However, you can register different sections for different page slugs and call do_settings_sections() with the appropriate slug based on the active tab.
How do I add a third tab?
Add a third <a> element to nav-tab-wrapper with a new tab parameter value, register sections and fields for a new page, add an elseif condition in the callback, and include the new tab value in the allowed values whitelist.
Why use tabs instead of separate pages with submenus?
Tabs do not clutter the sidebar menu, switch instantly, and are intuitive for users. They are ideal for 2-4 groups of related settings within a single theme or plugin.
How do I style tabs to match WordPress design?
Use the native CSS classes nav-tab-wrapper, nav-tab, and nav-tab-active. WordPress automatically applies standard admin panel styles to these classes. No additional styling is required.
Is h2 required for the tab container?
No, WordPress uses h2 on the Themes and Plugins pages, but it is not a requirement. You can use div — nav-tab-wrapper styles work on any block element. However, h2 is semantically more correct for a navigation heading.
How do I prevent LFI attacks through the tab parameter?
Always validate the tab value via in_array() against a whitelist of allowed values before using it in a function name, file path, or page name. Never pass $_GET['tab'] directly to include, require, or file_get_contents.



