The Customizer & Site Editor
The Customizer API still powers classic-theme options and plugin settings; block themes use the Site Editor instead.
add_action( 'customize_register', function ( WP_Customize_Manager $wp_customize ) { ... } );The Customizer is the live-preview settings screen classic themes use for theme options. You register on the customize_register action (WP 3.4), which passes the WP_Customize_Manager instance.
The three objects
A setting is the stored value plus its sanitisation. A section is a group in the sidebar. A control is the input bound to a setting. Register in that order: add_setting() before add_control(), because the control resolves its setting by ID.
Storage and transport
Settings store as a theme_mod by default, namespaced to the active theme and read with get_theme_mod() — the right choice for anything presentational, since it disappears with the theme. Use 'type' => 'option' for data that must survive a theme switch. transport is refresh (reload the preview iframe) or postMessage (update it with JavaScript).
Where it still applies
Classic themes, and plugins that add options. Since WordPress 5.9 the Appearance → Customize menu item is hidden when a block theme is active unless a plugin registers Customizer content. Block themes use the Site Editor: templates and parts through the editor, colours and typography through Global Styles, both stored in the database.
Example
<?php
/**
* Theme options in the Customizer.
*
* @param WP_Customize_Manager $wp_customize The manager instance.
*/
function mytheme_customize_register( $wp_customize ) {
$wp_customize->add_section(
'mytheme_footer',
array(
'title' => __( 'Footer', 'mytheme' ),
'description' => __( 'Text shown below the content.', 'mytheme' ),
'priority' => 130, // higher numbers sit lower in the sidebar
)
);
// Setting first — the control below looks it up by this ID.
$wp_customize->add_setting(
'mytheme_footer_text',
array(
'type' => 'theme_mod', // or 'option'
'capability' => 'edit_theme_options',
'default' => '',
'transport' => 'postMessage', // needs preview JS
'sanitize_callback' => 'sanitize_text_field', // never omit this
)
);
$wp_customize->add_control(
'mytheme_footer_text',
array(
'label' => __( 'Footer text', 'mytheme' ),
'section' => 'mytheme_footer',
'type' => 'text', // text, textarea, checkbox, select, radio, email, url, number
)
);
}
add_action( 'customize_register', 'mytheme_customize_register' );
// Reading it in a template. Second argument is the fallback.
$footer_text = get_theme_mod( 'mytheme_footer_text', __( '© Us', 'mytheme' ) );
echo esc_html( $footer_text );When to use it
- Adding a footer text or layout option to a classic theme
- Registering plugin settings that need a live preview
- Explaining why Appearance → Customize vanished after switching to a block theme
- Replacing a fragile postMessage binding with selective refresh
More examples
Making postMessage actually update the preview
This is the half everyone forgets. Without it, postMessage gives you a control that stores its value correctly but shows no live change, and the difference only surfaces after a save and reload. If you do not want to write the JS, use transport => 'refresh' — slower preview, no bugs.
<?php
// Load the binding script only inside the preview iframe.
add_action( 'customize_preview_init', function () {
wp_enqueue_script(
'mytheme-customize-preview',
get_theme_file_uri( 'assets/js/customize-preview.js' ),
array( 'customize-preview' ), // core handle: provides wp.customize
wp_get_theme()->get( 'Version' ),
array( 'in_footer' => true )
);
} );
/* assets/js/customize-preview.js
( function ( $ ) {
wp.customize( 'mytheme_footer_text', function ( value ) {
value.bind( function ( to ) {
$( '.site-footer__text' ).text( to );
} );
} );
} )( jQuery );
*/Sanitising a select, and theme_mod vs option
sanitize_callback runs on save and is the only validation the Customizer performs — there is no separate validation step for basic controls. For a select, whitelist against the same $choices array you passed to the control so the two can never drift apart.
<?php
add_action( 'customize_register', function ( $wp_customize ) {
$layouts = array(
'wide' => __( 'Wide', 'mytheme' ),
'boxed' => __( 'Boxed', 'mytheme' ),
);
$wp_customize->add_setting( 'mytheme_layout', array(
'default' => 'wide',
'transport' => 'refresh',
// A whitelist, not a filter: anything unrecognised falls back to the default.
'sanitize_callback' => static function ( $value ) use ( $layouts ) {
return array_key_exists( $value, $layouts ) ? $value : 'wide';
},
) );
$wp_customize->add_control( 'mytheme_layout', array(
'label' => __( 'Layout', 'mytheme' ),
'section' => 'mytheme_footer',
'type' => 'select',
'choices' => $layouts,
) );
} );
// Reading a theme_mod vs an option — same Customizer API, different storage.
$layout = get_theme_mod( 'mytheme_layout', 'wide' ); // per-theme, in the theme_mods_{slug} option
$api_key = get_option( 'myplugin_api_key', '' ); // survives a theme switchSelective refresh: the middle ground
Selective refresh re-renders just the matched element on the server instead of reloading the whole iframe or duplicating your PHP rendering in JavaScript. Use it when the markup is non-trivial — a menu, a post list — and keeping a JS copy of the template in sync would be a maintenance problem.
<?php
add_action( 'customize_register', function ( $wp_customize ) {
// Requires the setting's transport to be 'postMessage'.
if ( ! isset( $wp_customize->selective_refresh ) ) {
return; // WP 4.5+
}
$wp_customize->selective_refresh->add_partial( 'mytheme_footer_text', array(
'selector' => '.site-footer__text', // element to swap
'settings' => array( 'mytheme_footer_text' ),
'render_callback' => static function () {
return esc_html( get_theme_mod( 'mytheme_footer_text', '' ) );
},
) );
} );
Discussion