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
More Code Snippets
-
Reorder the Columns on the WooCommerce Orders List
Put your WooCommerce orders list columns in whatever order works…
-
Set the Default Country on WooCommerce Checkout
Pre-fill the WooCommerce checkout country (and optionally state) dropdown with…
-
Bulk Delete Expired Unused WooCommerce Coupons (Batched, Safe)
One-time bulk cleanup utility for stores with thousands of expired,…