Change Any Text on WooCommerce Checkout Blocks Without Plugins

If you’re using WooCommerce checkout blocks and want to change the text on any heading or label, you’ve probably realized the old gettext filter doesn’t work anymore. That’s because checkout blocks render in JavaScript, not PHP templates. The translations happen client-side, so your PHP filters never see them.

You might see people suggesting CSS tricks where you hide the original text and use ::before to show new text. That works but it’s messy. The text is still there in the HTML, screen readers still announce it, and you’re just covering it up with CSS.

This snippet uses a mutation observer to watch the DOM and replace text as checkout blocks load. It’s still a workaround, not an official API. WooCommerce checkout blocks don’t offer much customization flexibility yet, so this is the most practical way to change text without installing a plugin or waiting for them to add proper filters.

It handles dynamic content too. When someone toggles the “Use same address for billing” checkbox and the billing section appears, the observer catches it and replaces text instantly. Same thing happens with any other content that shows or hides during checkout.

/**
 * Change Checkout Block Text Without Flashing
 * 
 * Problem: Checkout blocks render in React/JS so gettext filters don't work
 * Solution: Use mutation observer to replace text in real-time as blocks load
 * 
 * Works for ANY text in checkout blocks, not just headings
 * Handles dynamic content that shows/hides during checkout
 */

add_action('wp_footer', function() {
    // Only run on checkout page
    if (!is_checkout()) {
        return;
    }
    
    // Configuration - Edit these to change any text
    $text_replacements = [
        'Contact information' => 'Your Details',
        'Billing address'     => 'Billing Information',
        'Shipping address'    => 'Delivery Address',
        'Shipping options'    => 'Delivery Options',
        'Payment options'     => 'Payment Method',
        // Add more as needed - works for ANY text in checkout blocks
    ];
    
    ?>
    <script id="carticy-checkout-text-replacer">
    (function() {
        'use strict';
        
        const textReplacements = <?php echo json_encode($text_replacements, JSON_UNESCAPED_UNICODE); ?>;
        const processedNodes = new WeakSet();
        let checkoutContainer = null;
        let isProcessing = false;
        
        // Replace text in a single text node
        function replaceTextNode(node) {
            if (processedNodes.has(node)) {
                return false;
            }
            
            const trimmedText = node.textContent.trim();
            if (trimmedText && textReplacements.hasOwnProperty(trimmedText)) {
                node.textContent = textReplacements[trimmedText];
                processedNodes.add(node);
                return true;
            }
            
            // Mark as processed even if no replacement to avoid re-checking
            processedNodes.add(node);
            return false;
        }
        
        // Process all text nodes in a container
        function processContainer(container) {
            if (!container || isProcessing) return;
            
            isProcessing = true;
            let replacements = 0;
            
            const walker = document.createTreeWalker(
                container,
                NodeFilter.SHOW_TEXT,
                {
                    acceptNode: function(node) {
                        // Skip empty or whitespace-only nodes
                        if (!node.textContent.trim()) {
                            return NodeFilter.FILTER_REJECT;
                        }
                        // Skip if already processed
                        if (processedNodes.has(node)) {
                            return NodeFilter.FILTER_REJECT;
                        }
                        return NodeFilter.FILTER_ACCEPT;
                    }
                },
                false
            );
            
            let node;
            while (node = walker.nextNode()) {
                if (replaceTextNode(node)) {
                    replacements++;
                }
            }
            
            isProcessing = false;
            return replacements;
        }
        
        // Handle mutations
        function handleMutations(mutations) {
            if (isProcessing) return;
            
            for (let i = 0; i < mutations.length; i++) {
                const mutation = mutations[i];
                
                // Handle added nodes
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    for (let j = 0; j < mutation.addedNodes.length; j++) {
                        const node = mutation.addedNodes[j];
                        
                        // Process text nodes directly
                        if (node.nodeType === 3) {
                            replaceTextNode(node);
                        }
                        // Process element nodes and their children
                        else if (node.nodeType === 1) {
                            processContainer(node);
                        }
                    }
                }
                
                // Handle direct text changes (when React updates existing text nodes)
                else if (mutation.type === 'characterData' && mutation.target.nodeType === 3) {
                    // Remove from processed set to allow re-processing
                    processedNodes.delete(mutation.target);
                    replaceTextNode(mutation.target);
                }
            }
        }
        
        // Debounced mutation handler for performance
        let mutationTimeout;
        const debouncedHandler = function(mutations) {
            clearTimeout(mutationTimeout);
            mutationTimeout = setTimeout(function() {
                handleMutations(mutations);
            }, 0);
        };
        
        // Create observer
        const observer = new MutationObserver(debouncedHandler);
        
        // Initialize
        function init() {
            // Find checkout container
            checkoutContainer = document.querySelector('.wc-block-components-form, .wc-block-checkout');
            
            if (checkoutContainer) {
                // Process existing content first
                processContainer(checkoutContainer);
                
                // Then observe for changes
                observer.observe(checkoutContainer, {
                    childList: true,
                    subtree: true,
                    characterData: true,
                    characterDataOldValue: false,
                    attributes: false,
                    attributeOldValue: false
                });
            } else {
                // If container not found, observe body temporarily
                observer.observe(document.body, {
                    childList: true,
                    subtree: true,
                    characterData: false,
                    attributes: false
                });
                
                // Check again after a short delay
                setTimeout(function() {
                    if (!checkoutContainer) {
                        checkoutContainer = document.querySelector('.wc-block-components-form, .wc-block-checkout');
                        if (checkoutContainer) {
                            observer.disconnect();
                            processContainer(checkoutContainer);
                            observer.observe(checkoutContainer, {
                                childList: true,
                                subtree: true,
                                characterData: true,
                                characterDataOldValue: false,
                                attributes: false,
                                attributeOldValue: false
                            });
                        }
                    }
                }, 100);
            }
        }
        
        // Start when ready
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', init);
        } else {
            init();
        }
        
        // Cleanup on page unload
        window.addEventListener('beforeunload', function() {
            observer.disconnect();
        });
    })();
    </script>
    <?php
}, 999);

The snippet uses a WeakSet to track which text nodes it’s already processed. This prevents it from replacing the same text over and over, but still allows re-processing when React updates a text node with new content. That’s what happens when sections appear or disappear.

The mutation observer watches for two types of changes. When new nodes get added to the page, it processes them immediately. When existing text nodes change their content, it removes them from the processed set and replaces the text again. This is how it catches the billing address heading when it appears after unchecking that checkbox.

Performance is handled by debouncing the mutation handler and using a TreeWalker with a custom filter. The observer only watches the checkout container, not the entire page. Empty text nodes and already processed nodes get filtered out immediately, so the code only processes what actually needs to change.

To use it, just add your text replacements to the array at the top. The left side is the original text exactly as it appears in checkout blocks. The right side is what you want to replace it with. You can change headings, labels, descriptions, button text, anything that’s visible text in the checkout blocks.

This only works with checkout blocks. If you’re using the classic checkout shortcode, you don’t need this snippet. Just use the regular gettext filter for classic checkout.

Need Help?

Learn how to add custom code to WordPress or reach out for custom development help.


About the Author

Ali Khallad

Ali Khallad

I’m Ali Khallad, a WordPress developer who’s been building custom plugins and WooCommerce solutions for over 10 years. I created Mega Forms and several extensions you’ll find on Carticy. I love solving tricky WordPress problems and sharing what I learn along the way. When I’m not coding, you’ll find me travelling, riding horses, or hunting down the best local food wherever I am.


More Code Snippets