The Customizer (Appearance → Customize) gives users a live preview while they change theme options. Classic themes still use it heavily; block themes use the Site Editor and theme.json instead. If you build or maintain a classic theme, this is how to add your own options properly.
The building blocks
- Panel — a top-level group (optional), e.g. “Theme Options”.
- Section — a group of controls inside a panel, e.g. “Header”.
- Setting — the stored value (a theme mod or an option) with its default and sanitize callback.
- Control — the UI element bound to a setting (text field, checkbox, colour picker, image upload…).
A complete example
add_action( 'customize_register', function ( WP_Customize_Manager $wp_customize ) {
$wp_customize->add_section( 'mytheme_header', [
'title' => __( 'Header', 'mytheme' ),
'priority' => 30,
] );
// Text setting
$wp_customize->add_setting( 'mytheme_tagline_text', [
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
'transport' => 'postMessage', // live update without full reload
] );
$wp_customize->add_control( 'mytheme_tagline_text', [
'label' => __( 'Header tagline', 'mytheme' ),
'section' => 'mytheme_header',
'type' => 'text',
] );
// Checkbox
$wp_customize->add_setting( 'mytheme_sticky_header', [
'default' => false,
'sanitize_callback' => fn( $v ) => (bool) $v,
] );
$wp_customize->add_control( 'mytheme_sticky_header', [
'label' => __( 'Sticky header', 'mytheme' ),
'section' => 'mytheme_header',
'type' => 'checkbox',
] );
// Colour
$wp_customize->add_setting( 'mytheme_accent', [
'default' => '#1d4ed8',
'sanitize_callback' => 'sanitize_hex_color',
] );
$wp_customize->add_control( new WP_Customize_Color_Control( $wp_customize, 'mytheme_accent', [
'label' => __( 'Accent colour', 'mytheme' ),
'section' => 'colors',
] ) );
} );
Read values in templates with get_theme_mod():
<?php if ( $tagline = get_theme_mod( 'mytheme_tagline_text' ) ) : ?>
<p class="site-tagline"><?php echo esc_html( $tagline ); ?></p>
<?php endif; ?>
Always sanitize
Every setting needs a sanitize_callback. The Theme Review guidelines require it, and it protects against stored XSS. Common callbacks: sanitize_text_field, sanitize_hex_color, absint, esc_url_raw, wp_kses_post for limited HTML. For select/radio controls, check the value is one of the allowed choices:
function mytheme_sanitize_choice( $value, $setting ) {
$choices = $setting->manager->get_control( $setting->id )->choices;
return array_key_exists( $value, $choices ) ? $value : $setting->default;
}
Live preview: postMessage and selective refresh
With 'transport' => 'refresh' (default), the preview reloads on every change. Two faster options:
postMessage with a small script that updates the DOM directly:
// enqueued on customize_preview_init
wp.customize( 'mytheme_tagline_text', ( value ) => {
value.bind( ( to ) => { document.querySelector( '.site-tagline' ).textContent = to; } );
} );
Selective refresh re-renders just one part of the page through PHP — simpler when markup is complex:
$wp_customize->selective_refresh->add_partial( 'mytheme_tagline_text', [
'selector' => '.site-tagline',
'render_callback' => fn() => esc_html( get_theme_mod( 'mytheme_tagline_text' ) ),
] );
Output CSS from settings
add_action( 'wp_head', function () {
$accent = sanitize_hex_color( get_theme_mod( 'mytheme_accent', '#1d4ed8' ) );
echo "<style>:root{--accent:{$accent};}</style>";
} );
Use CSS custom properties so one value updates the whole theme.
Customizer vs theme.json
For block themes, colours, typography and spacing belong in theme.json and are edited in the Site Editor — the Customizer is hidden by default. Use the Customizer only for classic or hybrid themes, or for options that theme.json can’t express (e.g. a toggle that changes PHP template behaviour).
Summary
Group options into sections, give every setting a sanitize callback, and use postMessage or selective refresh for a fast preview. For more theme development, see WordPress shortcodes and custom post meta. Developing on your own server? A WordPress VPS gives you a full staging environment with root access.