Contents
Introduction
This article is a comprehensive, practical guide to extending the WordPress Customizer in PHP with sections and controls. It covers the Customizer API basics, how to register sections, settings and controls, built-in control types, custom control classes, live preview using postMessage, selective refresh partials, sanitize callbacks, active callbacks, enqueuing assets, and practical examples you can copy into your theme or plugin.
Prerequisites
- WordPress 4.7 (recommended to use the latest stable WP).
- Familiarity with PHP, theme development and the WordPress hooks system.
- Basic knowledge of JavaScript for the live preview (postMessage) and selective refresh.
- An editor to add code to your themes functions.php or a plugin file.
Core concepts — whats involved
- WP_Customize_Manager: the object passed into the callback that manages settings, sections, controls and partials.
- Setting: a stored value (option or theme_mod) with optional transport and sanitation.
- Control: the UI element in the customizer where users change a setting.
- Section: groups controls. Panels are higher-level groupings that contain sections.
- Sanitize callbacks: validate and clean up user input before saving.
- Transport: how a setting is previewed — refresh (reload) or postMessage (JS live update).
- Selective Refresh: a compromise between full refresh and postMessage replaces DOM fragments returned by PHP render callbacks.
Where to add code
You can add the code in your themes functions.php, in a custom plugin, or in a new file included by either. If building a theme, place long code in a file like inc/customizer.php and include it from functions.php.
Basic example: Add a section, setting and a text control
Minimal example showing a text setting that stores a theme_mod and appears in a section.
add_section( mytheme_header_section, array( title => __( Header Settings, mytheme ), priority => 30, description => __( Configure header text and visibility., mytheme ), ) ) // Add a setting wp_customize->add_setting( mytheme_header_text, array( default => __( Welcome to my site, mytheme ), type => theme_mod, // or option capability => edit_theme_options, sanitize_callback => sanitize_text_field, transport => refresh, // or postMessage for live JS updates ) ) // Add a control (text input) wp_customize->add_control( mytheme_header_text_control, array( label => __( Header Text, mytheme ), section => mytheme_header_section, settings => mytheme_header_text, type => text, priority => 10, ) ) } add_action( customize_register, mytheme_customize_register_basic )
Notes on this basic example
- type = theme_mod stores the value with get_theme_mod() use option to store in wp_options with get_option().
- transport = refresh will reload the preview iframe on change. postMessage requires JS to update the preview live.
- sanitize_callback should always be used to avoid storing unsafe input.
Built-in control types
- text, textarea, checkbox, radio, select, dropdown-pages
- WP_Customize_Color_Control (color picker)
- WP_Customize_Image_Control (single image upload)
- WP_Customize_Upload_Control (generic file upload)
- WP_Customize_Cropped_Image_Control (image with cropping)
Example: Color control with live preview (postMessage) and JS
This example demonstrates a color setting that updates via postMessage so the preview updates instantly without a reload.
add_setting( mytheme_link_color, array( default => #0073aa, type => theme_mod, capability => edit_theme_options, sanitize_callback => sanitize_hex_color, transport => postMessage, // use postMessage for live preview ) ) wp_customize->add_control( new WP_Customize_Color_Control( wp_customize, mytheme_link_color_control, array( label => __( Link Color, mytheme ), section => colors, settings => mytheme_link_color, ) ) ) } add_action( customize_register, mytheme_customize_register_color ) ?>
Now enqueue the JS that listens to postMessage changes in the preview and applies them:
( function( ) { wp.customize( mytheme_link_color, function( value ) { value.bind( function( newval ) { ( a ).css( color, newval ) } ) } ) } )( jQuery )
Enqueue this script using customize_preview_init:
Selective Refresh (recommended over full refresh)
Selective refresh updates only parts of the preview DOM when a setting changes. It requires specifying a partial with a selector and a PHP render callback that echoes the HTML fragment.
selective_refresh ) ) { // Add a setting first wp_customize->add_setting( mytheme_site_tagline, array( default => get_bloginfo( description ), type => theme_mod, sanitize_callback => wp_kses_post, transport => postMessage, ) ) // Add a control wp_customize->add_control( mytheme_site_tagline_control, array( label => __( Site Tagline, mytheme ), section => title_tagline, settings => mytheme_site_tagline, type => text, ) ) // Register a partial wp_customize->selective_refresh->add_partial( mytheme_site_tagline_partial, array( selector => .site-tagline, settings => array( mytheme_site_tagline ), render_callback => mytheme_render_site_tagline, fallback_refresh => true, ) ) } } add_action( customize_register, mytheme_customize_register_partials ) // Render callback used by selective refresh function mytheme_render_site_tagline() { echo esc_html( get_theme_mod( mytheme_site_tagline, get_bloginfo( description ) ) ) } ?>
Key points about selective refresh
- The selector should match an element in your themes front-end template that holds the markup for the partial.
- render_callback must return or echo the HTML fragment that replaces the DOM element matched by selector.
- If partials fail, fallback_refresh can reload the whole preview.
Custom control class (PHP) — create custom UI in the Customizer pane
Extend WP_Customize_Control and override render_content() to create custom controls using PHP. Useful for complex inputs like grouped options, repeaters, or markup containing multiple fields.
label ) ) { echo . esc_html( this->label ) . } if ( ! empty( this->description ) ) { echo . esc_html( this->description ) . } // Simple divider markup echo } } endif // Register the control function mytheme_register_divider_control( wp_customize ) { wp_customize->add_section( mytheme_misc_section, array( title => __( Misc, mytheme ), priority => 160, ) ) // Add a dummy setting (controls must be associated with a setting) wp_customize->add_setting( mytheme_divider_dummy, array( default => , sanitize_callback => wp_kses_post, transport => refresh, ) ) // Add the divider control wp_customize->add_control( new MyTheme_Divider_Control( wp_customize, mytheme_divider, array( label => __( Section Divider, mytheme ), section => mytheme_misc_section, settings => mytheme_divider_dummy, priority => 10, ) ) ) } add_action( customize_register, mytheme_register_divider_control ) ?>
Complex custom controls
- When building complex controls that require JS interactivity (repeaters, sortable lists, color palettes), output the HTML in render_content and enqueue supporting JS/CSS for the Customizer controls panel using customize_controls_enqueue_scripts.
- Use this->input_attrs and this->link() to produce inputs that stay synced with the setting.
- Provide an accessible UI and support for sanitize callbacks for any data you persist.
Image and upload controls
Use built-in WP_Customize_Image_Control and WP_Customize_Upload_Control for media fields.
add_section( mytheme_media_section, array( title => __( Media, mytheme ), priority => 40, ) ) // Image control wp_customize->add_setting( mytheme_header_image, array( default => , sanitize_callback => absint, transport => refresh, ) ) wp_customize->add_control( new WP_Customize_Image_Control( wp_customize, mytheme_header_image_control, array( label => __( Header Image, mytheme ), section => mytheme_media_section, settings => mytheme_header_image, ) ) ) } add_action( customize_register, mytheme_customize_media_controls ) ?>
Sanitize callbacks and validation
Always sanitize data. Use core functions when available and write custom sanitization for complex types.
manager->get_control( setting->id )->choices return ( array_key_exists( input, choices ) ? input : setting->default ) } function mytheme_sanitize_hex_color_allow_empty( color ) { if ( === color ) { return } return sanitize_hex_color( color ) } ?>
Active callbacks and capability controls
Use active_callback to hide or show controls dynamically. Capability determines who can edit the setting.
add_setting( mytheme_show_sidebar, array( default => true, sanitize_callback => mytheme_sanitize_checkbox, ) ) wp_customize->add_control( mytheme_show_sidebar_control, array( label => __( Show Sidebar, mytheme ), section => layout, type => checkbox, settings => mytheme_show_sidebar, ) ) // Another control only shown when sidebar is enabled wp_customize->add_setting( mytheme_sidebar_width, array( default => 300, sanitize_callback => absint, ) ) wp_customize->add_control( mytheme_sidebar_width_control, array( label => __( Sidebar width (px), mytheme ), section => layout, settings => mytheme_sidebar_width, type => number, active_callback => function() { return get_theme_mod( mytheme_show_sidebar, true ) }, ) ) ?>
Enqueueing Customizer assets
Enqueue CSS and JS for both the controls panel (customize_controls_enqueue_scripts) and the preview pane (customize_preview_init). Use dependencies: customize-controls for controls, customize-preview for preview.
/ Example customizer-controls.css / .customize-control .customize-control-title { font-weight: 600 } .mytheme-image-preview { max-width: 100% height: auto }
Full practical example: a Theme_Customizer class
Organize your code into a class to keep things clean and avoid collisions. This example binds all features together: section, settings, controls, selective refresh, custom control, and asset enqueues.
add_section( mytheme_hero_section, array( title => __( Hero Section, mytheme ), priority => 30, ) ) // Heading setting control (selective refresh) wp_customize->add_setting( mytheme_hero_heading, array( default => __( Hello World, mytheme ), sanitize_callback => wp_kses_post, transport => postMessage, ) ) wp_customize->add_control( mytheme_hero_heading_control, array( label => __( Hero Heading, mytheme ), section => mytheme_hero_section, settings => mytheme_hero_heading, type => text, ) ) if ( isset( wp_customize->selective_refresh ) ) { wp_customize->selective_refresh->add_partial( mytheme_hero_heading_partial, array( selector => .hero .hero-heading, settings => array( mytheme_hero_heading ), render_callback => array( this, render_hero_heading ), ) ) } // Background image (cropped) wp_customize->add_setting( mytheme_hero_bg, array( default => , sanitize_callback => absint, transport => refresh, ) ) wp_customize->add_control( new WP_Customize_Cropped_Image_Control( wp_customize, mytheme_hero_bg_control, array( label => __( Hero Background, mytheme ), section => mytheme_hero_section, settings => mytheme_hero_bg, width => 1600, height => 600, flex_height => true, ) ) ) } public function render_hero_heading() { echo wp_kses_post( get_theme_mod( mytheme_hero_heading, __( Hello World, mytheme ) ) ) } public function customize_preview_js() { wp_enqueue_script( mytheme-customize-preview, get_template_directory_uri() . /assets/js/customize-preview.js, array( customize-preview, jquery ), 1.0, true ) } public function customize_controls_assets() { wp_enqueue_style( mytheme-customizer-controls, get_template_directory_uri() . /assets/css/customizer-controls.css, array(), 1.0 ) wp_enqueue_script( mytheme-customizer-controls, get_template_directory_uri() . /assets/js/customizer-controls.js, array( jquery, customize-controls ), 1.0, true ) } } // Initialize new MyTheme_Customizer() ?>
Preview JavaScript (for postMessage)
Simple JS for the Theme_Customizer example that listens for postMessage updates and updates DOM in the preview pane.
( function( ) { // Live update hero heading wp.customize( mytheme_hero_heading, function( value ) { value.bind( function( newVal ) { ( .hero .hero-heading ).text( newVal ) } ) } ) } )( jQuery )
List of commonly used WP_Customize_Manager methods and parameters
Method | Purpose |
add_section( id, args ) | Add a section to group controls |
add_panel( id, args ) | Add a panel that groups sections |
add_setting( id, args ) | Register a setting to store values |
add_control( id_or_control ) | Add a control. Pass an instance of WP_Customize_Control for complex controls |
remove_control( id ) | Remove a control |
remove_section( id ) | Remove a section |
selective_refresh->add_partial( id, args ) | Add a selective refresh partial |
Best practices
- Always sanitize inputs. Use built-in sanitizers when possible (sanitize_text_field, sanitize_hex_color, absint, esc_url_raw). For arrays use custom callbacks that whitelist keys and values.
- Prefer selective refresh for HTML fragments and postMessage for styling-only values (colors, font-sizes) that can be updated by JS.
- Use capability edit_theme_options (default) and avoid storing sensitive data in theme_mods without proper sanitization.
- Keep custom controls accessible (labels, descriptions, keyboard navigation).
- Enqueue scripts and styles only when needed (use proper hooks) to avoid loading assets unnecessarily.
- Namespace setting/control IDs with your theme/plugin prefix to avoid collisions.
Troubleshooting common issues
- Preview not updating with postMessage: ensure transport => postMessage is set and that preview JS exists and is enqueued via customize_preview_init.
- Select partial not working: verify the selector exists in the rendered page and the render_callback outputs exactly the markup fragment expected.
- Data not saving: check sanitize_callback — it may be rejecting values. Also ensure setting ID and control settings match.
- Custom control style broken: ensure CSS is enqueued via customize_controls_enqueue_scripts and check class names to prevent style conflicts.
Advanced topics (overview)
- Transport postMessage with JS for complex interactions — you can serialize objects into JSON and update several DOM nodes at once.
- Creating composite controls with multiple underlying settings — create a control that updates multiple settings via JS and binds them with links such as wp.customize( settingId ).set(…).
- Storing complex data structures — persist JSON-encoded arrays in a single setting and sanitize by validating expected structure.
- Custom selective refresh renderers — use output buffering to build markup in PHP then echo it.
- Adding contextual help and documentation inside the controls panel using description fields or custom controls with tutorial links.
Complete minimal checklist before shipping
- All settings have sanitize callbacks.
- All settings use appropriate transport for UX and performance.
- Custom controls enqueue only required JS/CSS and localize data if needed using wp_localize_script.
- Test Customizer in a clean environment and on mobile/responsive preview modes.
- Ensure theme templates reference get_theme_mod() where required and support fallback defaults and escaping (esc_html, esc_attr, esc_url).
Useful reference links
Conclusion
This article has covered everything required to extend the WordPress Customizer with sections and controls using PHP: registering settings, adding built-in and custom controls, enabling live preview via postMessage, using selective refresh for efficient updates, creating custom control classes, enqueuing supporting assets, and applying sanitization and active callbacks. Follow the provided examples and best practices to build a robust, user-friendly Customizer experience for your theme or plugin.
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |