How to preview Customizer changes with postMessage in JS in WordPress

Contents

Overview

This article is a complete, detailed tutorial for making WordPress Customizer changes live in the preview using the postMessage transport and JavaScript. It explains the difference between the postMessage transport and selective refresh, shows how to register settings for postMessage, how to enqueue and write the preview JavaScript, security and performance considerations, and several practical examples (site title, colors, complex DOM updates). All code examples are ready to copy into a theme or plugin.

What postMessage transport does

postMessage in the WordPress Customizer context means that when a control value changes in the Customizer controls pane, that value is sent instantly to the preview iframe via JavaScript instead of reloading the preview. Your themes preview script listens for that change and updates DOM/CSS immediately. This enables fast, smooth live preview without a full-page refresh.

When to use postMessage vs selective refresh

  • postMessage is ideal for client-side-only updates that can be implemented in JavaScript (text changes, color variables, showing/hiding elements, class changes, inline CSS updates).
  • Selective Refresh (partial refresh) is preferable when the markup for the changed area is best rendered by PHP (server-side output that requires theme logic). Selective refresh fetches HTML from the server for a registered partial and replaces the selectors content in the preview. It falls back to full refresh if JS is unavailable.
  • You can mix both: use postMessage for simple live JS updates and selective refresh when a PHP-rendered snippet must be replaced.

Prerequisites

  • WordPress theme or plugin where you can edit functions.php (or a plugin file) and add a JS file that is enqueued in the Customizer preview.
  • Familiarity with wp_customize API and basic JavaScript.

Step-by-step guide

1) Decide which settings will use postMessage

For each setting you want live-updated, set the transport to postMessage. You can do this when adding a setting or by modifying an existing settings transport.

Example: set transport on an existing setting

get_setting( blogname )->transport = postMessage

    // Add a custom setting with postMessage transport
    wp_customize->add_setting( mytheme_header_text, array(
        default           => Hello world,
        sanitize_callback => sanitize_text_field,
        transport         => postMessage,
    ) )

    // Add a simple control for the custom setting
    wp_customize->add_control( mytheme_header_text_control, array(
        label    => __( Header text, mytheme ),
        section  => title_tagline,
        settings => mytheme_header_text,
        type     => text,
    ) )
}
add_action( customize_register, mytheme_customize_register )
?>

2) Enqueue a preview script for the Customizer

Enqueue a JavaScript file that will run inside the preview iframe. Use the customize_preview_init hook and set customize-preview as a dependency so the wp.customize API is available.


3) Write the preview JavaScript

Your preview script uses the wp.customize JS API to listen for setting changes and update the DOM. Use value.bind or wp.customize( setting-id, function( value ) { … } ). Always update DOM via safe methods (textContent for text, style properties for CSS) to avoid injecting raw HTML from user input.

Basic example: live-updating the site title (vanilla JS)

( function() {
    // Wait until wp.customize is available (the dependency ensures this)
    wp.customize( blogname, function( value ) {
        value.bind( function( newVal ) {
            var titleEl = document.querySelector( .site-title a )
            if ( titleEl ) {
                // Use textContent to avoid HTML injection
                titleEl.textContent = newVal
            }
        } )
    } )
} )()

jQuery variant (commonly used in older themes)

( function(  ) {
    wp.customize( blogname, function( value ) {
        value.bind( function( newVal ) {
            ( .site-title a ).text( newVal )
        } )
    } )
} )( jQuery )

4) Live preview examples for common types of settings

Example: Background color

For color controls you can update inline styles or CSS variables. Using CSS variables is a clean approach if your theme uses them.

add_setting( mytheme_accent_color, array(
    default           => #0073aa,
    sanitize_callback => sanitize_hex_color,
    transport         => postMessage,
) )

wp_customize->add_control( new WP_Customize_Color_Control( wp_customize, mytheme_accent_color_control, array(
    label    => __( Accent Color, mytheme ),
    section  => colors,
    settings => mytheme_accent_color,
) ) )
?>
( function() {
    wp.customize( mytheme_accent_color, function( value ) {
        value.bind( function( newColor ) {
            // Update a CSS variable on :root
            document.documentElement.style.setProperty( --mytheme-accent, newColor )
            // or update an inline style fallback:
            // document.querySelector( header ).style.borderColor = newColor
        } )
    } )
} )()

Example: Toggling a class based on a checkbox

add_setting( mytheme_show_tagline, array(
    default           => true,
    sanitize_callback => wp_validate_boolean,
    transport         => postMessage,
) )

wp_customize->add_control( mytheme_show_tagline_control, array(
    label    => __( Show tagline, mytheme ),
    section  => title_tagline,
    settings => mytheme_show_tagline,
    type     => checkbox,
) )
?>
( function() {
    wp.customize( mytheme_show_tagline, function( value ) {
        value.bind( function( show ) {
            var tagline = document.querySelector( .site-description )
            if ( tagline ) {
                if ( show ) {
                    tagline.classList.remove( hidden-by-customizer )
                } else {
                    tagline.classList.add( hidden-by-customizer )
                }
            }
        } )
    } )
} )()

Example: Replacing markup safely (text-only)

When changing text you should always set textContent rather than innerHTML to avoid XSS risks.

wp.customize( mytheme_header_text, function( value ) {
    value.bind( function( newText ) {
        var header = document.querySelector( .site-header h1 )
        if ( header ) {
            header.textContent = newText // safe for user input
        }
    } )
} )

5) Handling complex updates and templates

For complex HTML that needs PHP logic (widgets, nav markup), consider selective refresh. If you must use postMessage, construct DOM safely in JS, or request new HTML via wp.apiFetch in the preview and insert it carefully. Keep content sanitized.

6) Add selective refresh partials where appropriate

Selective refresh replaces a selector with server-rendered markup. Use it when the area needs PHP rendering or depends on many theme functions. It still provides a near-live experience and is often easier for complex structures.

selective_refresh ) ) {
        wp_customize->selective_refresh->add_partial( blogname, array(
            selector        => .site-title a,
            render_callback => mytheme_customize_partial_blogname,
        ) )
    }
}
add_action( customize_register, mytheme_customize_partials )

function mytheme_customize_partial_blogname() {
    bloginfo( name )
}
?>

Security and sanitization

  • Always provide a sanitize_callback for settings to ensure only valid values are saved.
  • In the preview JS, prefer textContent over innerHTML for inserting untrusted text into the DOM.
  • For color values use sanitize_hex_color (PHP) and validate the input in JS if you accept external data.
  • If you fetch HTML from the server into the preview with XHR or wp.apiFetch, ensure that HTML was produced safely by your theme and properly escaped on output.

Performance considerations and best practices

  • Scope updates to the smallest possible DOM subtree. Avoid rerendering large sections repeatedly.
  • Cache selectors outside of the bind callback if the element is constant to avoid repeated DOM queries on high-frequency changes.
  • Debounce or throttle updates for rapidly-changing controls (sliders) to reduce layout thrashing:
( function() {
    function debounce( fn, wait ) {
        var timeout
        return function() {
            var ctx = this, args = arguments
            clearTimeout( timeout )
            timeout = setTimeout( function() {
                fn.apply( ctx, args )
            }, wait )
        }
    }

    wp.customize( mytheme_slider, function( value ) {
        var apply = debounce( function( v ) {
            var el = document.querySelector( .some-slider-affected )
            if ( el ) {
                el.style.width = v   px
            }
        }, 50 )

        value.bind( apply )
    } )
} )()

Troubleshooting

  1. If wp.customize is undefined in your preview script, make sure you enqueued it with the customize-preview dependency and used the customize_preview_init hook.
  2. If changes are not visible, verify that the settings transport is set to postMessage (not refresh).
  3. Inspect the preview iframe console for JavaScript errors — a JS exception will halt subsequent customizer bindings.
  4. If a change falls back to a full refresh, check if selective refresh partials exist or if the setting isnt tailored for postMessage updates.

Advanced topics

  • Control-side JS: You can add scripts to the controls pane (customize_controls_enqueue_scripts) to enhance controls they interact with settings via wp.customize too.
  • Transporting complex objects: Settings can hold arrays/objects if you use JSON controls. In JS you will receive the object and should handle it appropriately. Sanitize on save.
  • State and history: Customizer supports preview state in the URL be mindful when integrating non-deterministic behavior in your JS updates.

Practical checklist before shipping

  • Every setting with live preview has transport => postMessage.
  • Preview JS enqueued via customize_preview_init with dependency customize-preview.
  • JS updates use safe DOM APIs (textContent, setProperty for CSS variables, element.classList).
  • All inputs have server-side sanitize callbacks.
  • Performance: cached selectors, debounced handlers for rapid changes.
  • Fallback: ensure the site renders correctly on refresh (postMessage is only for preview experience).

Complete minimal example — functions.php preview JS

Drop these two pieces into your theme to implement a live-updating site title and accent color.

get_setting( blogname )->transport = postMessage

    // accent color setting
    wp_customize->add_setting( mytheme_accent_color, array(
        default           => #0073aa,
        sanitize_callback => sanitize_hex_color,
        transport         => postMessage,
    ) )

    wp_customize->add_control( new WP_Customize_Color_Control(
        wp_customize, mytheme_accent_color_control, array(
            label    => __( Accent Color, mytheme ),
            section  => colors,
            settings => mytheme_accent_color,
        )
    ) )
}
add_action( customize_register, mytheme_customize_register )

function mytheme_customize_preview_js() {
    wp_enqueue_script(
        mytheme-customizer-preview,
        get_template_directory_uri() . /js/customizer-preview.js,
        array( customize-preview ),
        1.0,
        true
    )
}
add_action( customize_preview_init, mytheme_customize_preview_js )
?>
// js/customizer-preview.js
( function() {
    // Update site title
    wp.customize( blogname, function( value ) {
        value.bind( function( newVal ) {
            var el = document.querySelector( .site-title a )
            if ( el ) { el.textContent = newVal }
        } )
    } )

    // Update accent color via CSS variable
    wp.customize( mytheme_accent_color, function( value ) {
        value.bind( function( newColor ) {
            document.documentElement.style.setProperty( --mytheme-accent, newColor )
        } )
    } )
} )()

Further reading



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

Your email address will not be published. Required fields are marked *