Once you have mastered the five basic input types in the WordPress Settings API, the next logical step is learning the helper functions and rendering patterns that make those inputs production-ready. This guide focuses on three areas that intermediate developers often get wrong: properly wiring checkboxes with the checked() function, building grouped radio button sets, and constructing select dropdowns that correctly restore saved state using the selected() function. We also cover multi-field grouped inputs — a pattern for organizing related fields that belong together.
Understanding the WordPress Helper Functions
WordPress provides three helper functions specifically designed to maintain form state across page reloads. These functions compare a reference value against a stored value and output the appropriate HTML attribute. They eliminate the tedious and error-prone process of manually writing conditional checked or selected attributes.
| Function | Signature | Output When Match | Used With |
|---|---|---|---|
| checked() | checked($checked, $current, $echo = true) | checked='checked' | <input type="checkbox"> and <input type="radio"> |
| selected() | selected($selected, $current, $echo = true) | selected='selected' | <option> elements inside <select> |
| disabled() | disabled($disabled, $current, $echo = true) | disabled='disabled' | Any form element when it should be non-interactive |
All three functions follow the same pattern: compare the first argument against the second, and if they match, output the corresponding HTML attribute. The third argument controls whether the function echoes its output directly or returns it as a string. In most cases, you want the default echoing behavior inside your PHP templates.
| Scenario | Function | Example Call | Result When Match |
|---|---|---|---|
| Checkbox checked | checked() | checked(1, get_option('my_checkbox')) | checked='checked' |
| Radio button selected | checked() | checked('left', get_option('layout')) | checked='checked' |
| Select option chosen | selected() | selected('roboto', get_option('font')) | selected='selected' |
| Field disabled | disabled() | disabled('locked', get_option('status')) | disabled='disabled' |
Checkboxes: Deep Dive with checked()
A single checkbox represents a boolean state — on or off, enabled or disabled, show or hide. The checked() function ensures that when a user has previously checked a box and saved the settings, the checkbox appears checked when they return to the settings page. Without it, every checkbox would default to unchecked regardless of the saved value, forcing users to re-check options every time they edit settings.
Basic Checkbox Rendering
function mytheme_checkbox_callback() {
$options = get_option('mytheme_options');
$current = isset($options['enable_feature']) ? $options['enable_feature'] : 0;
?>
Enables experimental features and developer tools. Use with caution.
The checked() function compares two values. When the first argument (1, the checkbox value when checked) equals the second argument ($current, the saved option value), it outputs checked='checked' as an HTML attribute. This triggers the browser to render the checkbox as checked. When they do not match, nothing is output, and the checkbox appears unchecked.Multiple Checkboxes (Checkbox Group)
Multiple related checkboxes present a different challenge. Unlike radio buttons where only one option can be selected, checkbox groups allow any combination. Each checkbox must have a unique name within the options array:
function mytheme_checkbox_group_callback() {
$options = get_option('mytheme_options');
$features = array(
'lazy_load' => 'Enable Lazy Loading for images',
'dark_mode' => 'Enable Dark Mode toggle',
'breadcrumbs' => 'Show Breadcrumb navigation',
'back_to_top' => 'Show Back to Top button',
'sticky_menu' => 'Use Sticky Header Menu',
);
?>
Each checkbox stores its state independently under its own array key. The sanitization callback must handle each key separately, defaulting to 0 when unchecked:
function mytheme_sanitize_checkbox_group($input) {
$output = array();
$feature_keys = array('lazy_load', 'dark_mode', 'breadcrumbs', 'back_to_top', 'sticky_menu');
foreach ($feature_keys as $key) {
$output[$key] = isset($input[$key]) ? absint($input[$key]) : 0;
}
return $output;
}
[/codeblock]
Radio Buttons: Building Proper Groups
Radio buttons enforce mutually exclusive selection. Users can pick exactly one option from a set. The browser handles the exclusivity automatically when all radio inputs in a group share the same name attribute. The checked() function, applied individually to each radio option, ensures the previously saved selection is correctly shown.
Radio Button Group with Labels
function mytheme_radio_group_callback() {
$options = get_option('mytheme_options');
$current = isset($options['sidebar_position']) ? $options['sidebar_position'] : 'right';
$positions = array(
'left' => 'Left Sidebar',
'right' => 'Right Sidebar',
'none' => 'No Sidebar (Full Width)',
);
?>
Controls where the sidebar appears on the page layout.
Every radio input in the group shares the same name — mytheme_options[sidebar_position]. This tells the browser they belong to the same group. The checked() function compares each option's value against the saved $current value. Only the matching radio button receives the checked attribute.Label Wiring: The id and for Relationship
Proper label wiring improves both accessibility and usability. When a label's for attribute matches an input's id attribute, clicking the label text toggles the associated input. This makes radio buttons and checkboxes significantly easier to use, especially on mobile devices where small click targets are frustrating.
/>
Right Layout
[/codeblock]
Select Dropdowns: Using selected()
Select dropdowns present a list of choices in a space-efficient format. The selected() function mirrors checked() for the select context: it compares the option value against the saved value and outputs selected='selected' when they match.
Basic Select with Iterated Options
function mytheme_select_dropdown_callback() {
$options = get_option('mytheme_options');
$current = isset($options['primary_font']) ? $options['primary_font'] : 'system-ui';
$fonts = array(
'system-ui' => 'System Default Font',
'georgia' => 'Georgia',
'helvetica' => 'Helvetica Neue',
'roboto' => 'Roboto',
'lora' => 'Lora',
'merriweather' => 'Merriweather',
'opensans' => 'Open Sans',
'montserrat' => 'Montserrat',
);
?>
The base font for all body text across the site.
The foreach loop pattern is the standard approach for select dropdowns. Define your options as an associative array, iterate through them, and call selected() for each option. This keeps your code DRY and makes adding or removing options as simple as editing the array.Select with Optgroups
For longer option lists, grouping related options under <optgroup> labels improves scannability:
function mytheme_grouped_select_callback() {
$options = get_option('mytheme_options');
$current = isset($options['content_width']) ? $options['content_width'] : 'medium';
$widths = array(
'Narrow' => array(
'600' => '600px',
'640' => '640px',
),
'Standard' => array(
'700' => '700px',
'760' => '760px',
'800' => '800px',
),
'Wide' => array(
'960' => '960px',
'1100' => '1100px',
'1200' => '1200px',
),
);
?>
Grouped Inputs: Multiple Related Fields
Sometimes a single logical setting requires multiple input fields. For example, a social media configuration might need a URL and a visibility toggle for each platform. Grouping these related fields visually and logically improves the user experience and the code organization.
Social Media Fields Group
function mytheme_social_group_callback() {
$options = get_option('mytheme_options');
$platforms = array(
'twitter' => 'Twitter / X',
'facebook' => 'Facebook',
'instagram' => 'Instagram',
'linkedin' => 'LinkedIn',
'youtube' => 'YouTube',
);
?>
This grouped approach stores related data in nested arrays: social_urls for the URLs and social_show for the visibility toggles. The sanitization callback processes these nested structures:
function mytheme_sanitize_social_group($input) {
$output = array();
$platforms = array('twitter', 'facebook', 'instagram', 'linkedin', 'youtube');
if (isset($input['social_urls']) && is_array($input['social_urls'])) {
foreach ($platforms as $key) {
if (isset($input['social_urls'][$key]) && !empty($input['social_urls'][$key])) {
$output['social_urls'][$key] = esc_url_raw($input['social_urls'][$key]);
} else {
$output['social_urls'][$key] = '';
}
}
}
if (isset($input['social_show']) && is_array($input['social_show'])) {
foreach ($platforms as $key) {
$output['social_show'][$key] = isset($input['social_show'][$key])
? absint($input['social_show'][$key])
: 0;
}
}
return $output;
}
[/codeblock]
Combining All Input Types: A Full Feature Set Page
Let us assemble a complete settings page that uses checkboxes, radio buttons, select dropdowns, and grouped inputs together. This is a realistic pattern for a theme options page controlling layout, typography, and social integration:
function mytheme_register_all_settings() {
register_setting(
'mytheme_options_group',
'mytheme_options',
'mytheme_full_sanitize'
);
add_settings_section(
'mytheme_feature_section',
'Feature Toggles',
'mytheme_feature_section_text',
'mytheme-full-settings'
);
add_settings_section(
'mytheme_layout_section',
'Layout Settings',
'mytheme_layout_section_text',
'mytheme-full-settings'
);
add_settings_section(
'mytheme_social_section',
'Social Media',
'mytheme_social_section_text',
'mytheme-full-settings'
);
add_settings_field(
'feature_checkboxes',
'Enabled Features',
'mytheme_feature_checkbox_group',
'mytheme-full-settings',
'mytheme_feature_section'
);
add_settings_field(
'sidebar_radio',
'Sidebar Position',
'mytheme_sidebar_radio_group',
'mytheme-full-settings',
'mytheme_layout_section'
);
add_settings_field(
'font_select',
'Primary Font',
'mytheme_font_select',
'mytheme-full-settings',
'mytheme_layout_section'
);
add_settings_field(
'social_group',
'Social Profiles',
'mytheme_social_group_callback',
'mytheme-full-settings',
'mytheme_social_section'
);
}
add_action('admin_init', 'mytheme_register_all_settings');
[/codeblock]
Each settings section groups related fields under a common heading. The section callbacks provide explanatory text. The field callbacks render the actual input elements. This separation of concerns keeps the code modular: you can add, remove, or reorder sections without touching the rendering logic of individual fields.
Registering the Admin Page
With all settings registered, the final step is creating the admin page that displays the form:
function mytheme_full_settings_page() {
?>
MyTheme Complete Settings
Complete Sanitization for All Field Types
A sanitization callback that handles checkboxes, radio buttons, selects, and nested arrays must be comprehensive and defensive:
function mytheme_full_sanitize($input) {
$output = array();
$feature_keys = array('lazy_load', 'dark_mode', 'breadcrumbs', 'back_to_top', 'sticky_menu');
foreach ($feature_keys as $key) {
$output[$key] = isset($input[$key]) ? absint($input[$key]) : 0;
}
$valid_sidebars = array('left', 'right', 'none');
$output['sidebar_position'] = isset($input['sidebar_position'])
&& in_array($input['sidebar_position'], $valid_sidebars)
? $input['sidebar_position']
: 'right';
$valid_fonts = array('system-ui', 'georgia', 'helvetica', 'roboto', 'lora', 'merriweather', 'opensans', 'montserrat');
$output['primary_font'] = isset($input['primary_font'])
&& in_array($input['primary_font'], $valid_fonts)
? $input['primary_font']
: 'system-ui';
$platforms = array('twitter', 'facebook', 'instagram', 'linkedin', 'youtube');
foreach ($platforms as $key) {
if (isset($input['social_urls'][$key]) && !empty($input['social_urls'][$key])) {
$output['social_urls'][$key] = esc_url_raw($input['social_urls'][$key]);
} else {
$output['social_urls'][$key] = '';
}
$output['social_show'][$key] = isset($input['social_show'][$key])
? absint($input['social_show'][$key])
: 0;
}
return $output;
}
[/codeblock]
Debugging Form State Issues
When checkboxes, radio buttons, or selects do not reflect the saved state, the cause is almost always a mismatch between the value compared by checked() or selected() and the actual stored option value. Here is a systematic debugging approach:
Step 1: Dump the Stored Option
Add a temporary var_dump(get_option('mytheme_options')) at the top of your rendering callback. Check that the value actually exists and has the expected type. A value of "1" (string) will not match a comparison against 1 (integer) in checked(1, $current) because PHP's strict comparison is used internally.
Step 2: Verify the Name Attribute Matches
If your input has name="mytheme_options[sidebar]" but your sanitizer checks $input['sidebar_position'], the value is stored under a different key than the one you are reading. The name attribute, the sanitizer key, and the get_option() key must all match exactly.
Step 3: Check the Sanitization Callback Runs
Add an error_log() call at the start of your sanitization callback. If it never appears in the debug log, the callback is not being invoked. This usually means the third argument to register_setting() is misspelled or the function is not defined when register_setting() runs.
Accessibility Considerations for Form Elements
Admin pages must be usable by everyone, including people relying on screen readers, keyboard navigation, and assistive technologies:
- Every <input>, <select>, and <textarea> must have an associated <label> element. Use either the wrapping approach or the for/id approach.
- Groups of related radio buttons and checkboxes must be wrapped in a <fieldset> with a <legend> element describing the group. Screen readers announce the legend when entering the fieldset.
- Error messages and validation feedback should use aria-describedby to link the message to the input field.
- Tab order should follow the visual order. Do not use positive tabindex values to reorder — restructure the HTML instead.
- Required fields should use the aria-required="true" attribute in addition to the HTML5 required attribute for broader screen reader support.
Frequently Asked Questions
Why does checked() not mark my checkbox even though the value is correct?
checked() compares values using loose comparison (==), so type matters. If your stored value is the string "1" but you call checked(1, $current), PHP compares string "1" with integer 1 and they match. However, if you store the integer 1 and call checked('1', $current), the comparison also succeeds. The real issue is usually not type but a misalignment between the name attribute in the form, the key in the sanitizer, and the key in the stored options array. Verify all three use the exact same key name.
How do I handle radio buttons when no default is set?
Always provide a default value when reading the option: $current = isset($options['layout']) ? $options['layout'] : 'default_value'. If no radio button appears selected, none of the checked() calls will output anything, and the browser will leave all radio buttons unchecked. This is valid HTML but confusing for users. Set a sensible default that matches the theme or plugin defaults.
Can I use checked() for both checkboxes and radio buttons?
Yes. The checked() function works for both input types. It outputs checked='checked' regardless of whether the input is type="checkbox" or type="radio". The function is generic—it does not inspect the input type—so you can use it consistently across all your form elements that need a checked attribute.
How do I handle select dropdowns with dynamic options from the database?
Fetch the options in your rendering callback using a database query or API call, build the options array dynamically, and iterate with foreach just like a static array. Be sure to validate the saved value against the dynamically built list in your sanitization callback. If options can change between saves, store option slugs rather than display labels, and rebuild the list from the database before validating.
What is the correct way to group multiple checkbox options?
Wrap all related checkboxes in a <fieldset> with a <legend>. Give each checkbox a unique name within the options array, such as mytheme_options[feature_lazy], mytheme_options[feature_dark], etc. Process each key independently in the sanitization callback. Unlike radio buttons, checkboxes in a group can all have different name attributes—and they should, because each represents an independent boolean setting under a different option key.
Why does my select dropdown always show the first option after saving?
The selected() function compares its first argument (the option value) against the second (the saved value). If they do not match, no selected attribute is output. Common causes: the saved value has extra whitespace (use trim() when saving), the value attribute contains a typo that differs from the whitelist, or the sanitization callback replaces the value with a default because the submitted value fails the in_array() whitelist check.
How do I add a "no selection" option to a select dropdown?
Add an option with an empty value as the first element: <option value="">-- Select One --</option>. Do not call selected() on this option unless the saved value is explicitly empty. In your sanitization callback, either allow the empty string as a valid value or replace it with a default: if (empty($input['field'])) { $input['field'] = 'default'; }.
How do I organize a form with 20+ settings fields without it becoming unmanageable?
Split fields across multiple settings sections using add_settings_section(). Group 3-7 related fields per section. Each section gets its own heading and explanatory text. Use multiple settings pages accessed through submenus for very large configurations. For extremely complex settings (50+ fields), consider using the WordPress Customizer instead of a standalone admin page—it handles grouping and state management natively.
Tap to react



