A WooCommerce store I maintain asked for something that sounded trivial: hide the Square wallet button — the Apple Pay and Google Pay one — from the single-product page. They only wanted plain card checkout. I figured this was a five-minute job. Add one CSS rule, display:none, done. I was very wrong.
.wallet-button, .apple-pay-button {
display: none !important;
}I deployed, refreshed the page, the button was gone. Satisfied, I closed the tab. Then the client's tester opened a different product page, and the wallet button was sitting there calmly under the price again. I opened the same page, hard-refreshed, sometimes it was gone, sometimes it flashed in for a split second before disappearing, sometimes it just stayed put and refused to leave. That inconsistency was the most frustrating part, because it meant the problem was not my selector.
The symptom
What I saw: the display:none !important rule was there — I checked in DevTools, the selector matched — but the button still showed. Sometimes. On the same reload it was sometimes hidden, sometimes not. Open the Elements panel while the button was visible, and the node was in the DOM with its own inline style overriding my CSS. I did not write that inline style. So something else was putting it there after the page finished loading.
My first instinct was a bad one, as usual: I assumed my CSS was losing on specificity, so I piled on !important and hardened the selector. No effect. I assumed caching, so I purged everything. No effect. Only after I really paid attention to the fact that the button flashed in a moment after everything else rendered did I realize I was fighting something that was not there when I wrote the rule.
Why this happens
The Square payment SDK does not render its wallet button in the initial HTML. It injects that button into the DOM asynchronously, after the page loads, after the SDK itself finishes fetching and initializing. So the timeline goes like this: the browser parses the HTML, applies my CSS to the elements that exist at that moment, the page looks ready. My display:none rule runs perfectly, but against a DOM that does not yet contain the wallet button. A few hundred milliseconds later, the Square SDK inserts its button — sometimes complete with its own inline style.
CSS is not a program that waits. It is applied once against what exists, and yes, ideally the browser applies rules to new nodes too, but the moment Square sets an inline style on its own element, my stylesheet rule loses to that inline style. On top of that, Square sometimes re-renders its button asynchronously — say, after the payment method changes — and each re-render drops a fresh node with no trace of my work on it. So it was not one moment I needed to handle, but a stream of unpredictable moments.
Once the logic clicked, the inconsistency made sense. If the reload was fast and the Square SDK was slow, my rule "won" because the button did not exist yet when I looked. If the SDK managed to inject before I looked, the button stayed. Nothing was broken. The only broken thing was my assumption that the element I wanted to hide was already in the DOM when my code ran.
The fix
The fix is not more aggressive CSS. There is no point hiding something that has not been born yet. The fix is to watch the container and remove the button the moment Square injects it, then keep watching so the next re-render gets caught too. A MutationObserver is exactly right for this: it tells me precisely when the DOM changes, so I can react to a node that appears later.
document.addEventListener('DOMContentLoaded', function () {
var container = document.querySelector('.payment-container');
if (!container) return;
var observer = new MutationObserver(function () {
var wallet = container.querySelector('.wallet-button, .apple-pay-button');
if (wallet) {
wallet.remove();
}
});
observer.observe(container, { childList: true, subtree: true });
});The difference is subtle but important: I am not hiding the button, I am removing the node from the DOM. Removing means there is no inline style left to fight, no element left to flash in. And because the observer stays alive, when Square re-renders and injects a new button, the callback runs again and removes it before it can show. That is what stopped both the flash and the inconsistent reappearance.
One last thing that matters: this script only needs to run on the single-product page, where the wallet button lives. Running the observer on every page is wasteful and can trigger unexpected side effects elsewhere. So I enqueue the script with a guard on the PHP side, only when is_product():
add_action( 'wp_enqueue_scripts', function () {
if ( ! is_product() ) {
return;
}
wp_enqueue_script(
'hide-square-wallet',
get_stylesheet_directory_uri() . '/js/hide-square-wallet.js',
array(),
'1.0.0',
true
);
} );The takeaway
If an element reappears even though your CSS clearly tells it to vanish, stop sharpening the selector and ask first: did this element even exist when my rule ran? Third-party SDKs like Square often inject UI asynchronously after load, and static CSS or a one-time JS query cannot catch something born later. When your opponent is a DOM that changes over time, the answer is a tool that also moves over time: a MutationObserver that watches the container, removes the node on appearance, and stays on guard for the next re-render. And always scope it — here a simple is_product() guard is enough — so your fix does not become a new problem on other pages.
