WordPress Customizer postMessage Transport: Instant Live Preview Without Page Reload

The WordPress Customizer previews changes in an iframe that re-renders the front end. By default, every change a user makes to a setting causes a full page reload inside the preview iframe — the browser fetches the page again with the new setting value, re-parses CSS, re-runs JavaScript, and replaces the entire iframe content. For slow sites or complex settings this produces a noticeable delay, degrading the editing experience. WordPress solves this with the postMessage transport: instead of a full reload, changes are transmitted as JavaScript window.postMessage events to the preview iframe, and a small JavaScript function in the theme updates the DOM directly — changing a CSS variable, swapping a colour, updating a text node — without any server round-trip. The result is instant, real-time preview updates. Implementing postMessage transport requires a PHP side (registering the transport) and a JavaScript side (handling the message and applying the DOM change).

Problem: Your theme has a Customizer colour picker for the primary brand colour. Currently every change triggers a full iframe reload, creating a 2–3 second delay. You want instant live preview updates.

Solution: Change the setting's transport from 'refresh' (default) to 'postMessage', then add a JavaScript handler in the Customizer preview that listens for the value and applies it to a CSS custom property or style attribute.

<?php
add_action( 'customize_register', 'register_brand_color_setting' );

function register_brand_color_setting( WP_Customize_Manager $wp_customize ) {

    $wp_customize->add_section( 'brand_colors', [
        'title'    => __( 'Brand Colors', 'textdomain' ),
        'priority' => 30,
    ] );

    $wp_customize->add_setting( 'primary_color', [
        'default'           => '#0066cc',
        'sanitize_callback' => 'sanitize_hex_color',
        'transport'         => 'postMessage', // ← instant preview, no reload
    ] );

    $wp_customize->add_control( new WP_Customize_Color_Control(
        $wp_customize,
        'primary_color',
        [
            'label'   => __( 'Primary Color', 'textdomain' ),
            'section' => 'brand_colors',
        ]
    ) );
}

// Enqueue the preview JS only inside the Customizer preview iframe
add_action( 'customize_preview_init', function () {
    wp_enqueue_script(
        'mytheme-customizer-preview',
        get_template_directory_uri() . '/js/customizer-preview.js',
        [ 'customize-preview' ],
        filemtime( get_template_directory() . '/js/customizer-preview.js' ),
        true
    );
} );

JavaScript file js/customizer-preview.js — runs inside the preview iframe:

( function ( $ ) {
    'use strict';

    // wp.customize is the Customizer JS API available in the preview
    wp.customize( 'primary_color', function ( value ) {
        // 'bind' fires whenever the setting value changes
        value.bind( function ( newColor ) {
            // Option A: update a CSS custom property on :root
            document.documentElement.style.setProperty( '--color-primary', newColor );

            // Option B: update all elements with a specific class directly
            $( '.btn-primary, .site-header' ).css( 'background-color', newColor );
        } );
    } );

    // Multiple settings can be bound independently:
    wp.customize( 'secondary_color', function ( value ) {
        value.bind( function ( newColor ) {
            document.documentElement.style.setProperty( '--color-secondary', newColor );
        } );
    } );

} )( jQuery );

NOTE: postMessage transport only updates the Customizer preview — it does not affect what gets saved to the database. The saved value is always applied server-side (via get_theme_mod() in your theme's CSS output). If you use postMessage, make sure your server-side CSS output and the JavaScript preview handler both apply the colour to the same elements, otherwise the preview will look different from the published result. For settings where the full server-rendered output is required (e.g. widget content, complex PHP template changes), keep the transport as 'refresh'.