← All posts

Tutorials

Disable dark patterns: fake urgency, sneaky checkboxes

"Only 2 left in stock." "12 people are viewing this right now." A countdown timer that resets every time you reload. A pre-checked box adding insurance you never asked for. These are dark patterns — interface tricks designed to rush and trick you. This article shows how to neutralize the common ones with CSS and JavaScript so you can shop with a clear head.

What counts as a dark pattern

A dark pattern is a UI choice that benefits the business at your expense by exploiting how attention and defaults work. The ones worth disarming on a shopping page:

You cannot stop a site from being designed this way, but you can edit how it renders in your browser.

Spotting a fake countdown

A real deadline is the same for everyone and survives a reload. A fake one resets. Quick test: note the timer, reload the page, look again. If it jumped back to "05:00", it is theatre. Open DevTools, Elements panel, and find the element whose text ticks down — note its class.

Hiding fake-urgency widgets with CSS

The fastest fix is to make the pressure widgets disappear entirely. They are almost always cleanly separated, named-for-what-they-are elements:

/* Fake urgency, scarcity and social-proof widgets */
.countdown-timer,
.sale-timer,
.urgency-banner,
.stock-warning,
.low-stock,
.viewing-now,
.recently-viewed-count,
[class*="countdown" i],
[class*="urgency" i],
[class*="scarcity" i],
[class*="viewers" i],
[class*="people-viewing" i] {
  display: none !important;
}

As always, the i flag makes attribute matching case-insensitive. Start with this block; it clears the visible noise on most retail pages instantly.

Freezing a countdown instead of hiding it

Sometimes you want to see that a sale exists but not be hurried by a ticking clock. Freeze it: stop the script that updates it. The blunt, reliable approach is to neutralize the timer functions before the page's script starts, at document_start:

// Freeze countdowns: defang the timers the page uses to tick
const realSetInterval = window.setInterval;

window.setInterval = function (fn, delay, ...args) {
  // Block fast-ticking timers (countdowns update ~every second)
  if (delay && delay <= 1000) {
    console.log('[dark-patterns] blocked a 1s interval');
    return -1;            // a harmless fake id
  }
  return realSetInterval.call(this, fn, delay, ...args);
};

This is deliberately broad — it stops every sub-second interval, which on a product page is almost always a countdown. If it freezes something you need, narrow it by checking fn.toString() for keywords like countdown before blocking.

A gentler alternative: let the timer run but pin its displayed text once, then stop updates to that node:

// Pin the countdown text and stop it changing
const timer = document.querySelector('[class*="countdown" i]');
if (timer) {
  const frozen = timer.textContent;
  new MutationObserver(() => {
    if (timer.textContent !== frozen) timer.textContent = frozen;
  }).observe(timer, { childList: true, subtree: true, characterData: true });
}

Un-checking sneaky add-on boxes

The most expensive dark pattern is the pre-checked checkbox: shipping insurance, a "round up for charity", a recurring membership. It is checked by default and worded so you skim past it. Un-check them with JavaScript after the page loads:

// Un-tick pre-checked add-on checkboxes
function unchargeMe() {
  document.querySelectorAll('input[type="checkbox"]:checked')
    .forEach(box => {
      const label = (box.closest('label')?.textContent
        || box.parentElement?.textContent || '').toLowerCase();

      // Only touch boxes that smell like a paid add-on
      if (/insurance|protection|warranty|donation|round up|express|priority|membership|subscribe/.test(label)) {
        box.checked = false;
        // Fire events so the page recalculates the total
        box.dispatchEvent(new Event('change', { bubbles: true }));
        box.dispatchEvent(new Event('input', { bubbles: true }));
        console.log('[dark-patterns] un-checked:', label.trim().slice(0, 60));
      }
    });
}

unchargeMe();
// Re-run when the cart re-renders
new MutationObserver(unchargeMe)
  .observe(document.body, { childList: true, subtree: true });

The change and input events matter — without them the page total may still include the add-on even though the box looks unchecked. Always verify the price updated.

Defusing confirmshaming wording

You cannot easily rewrite text, but you can stop confirmshaming from doing its job: make the decline option as visible and neutral as the accept option. If a site styles "No thanks" as tiny gray text, restyle it:

/* Make the decline link a normal, readable button */
.modal a[class*="decline" i],
.modal button[class*="dismiss" i],
[class*="confirm-shame" i] {
  font-size: 1rem !important;
  color: inherit !important;
  text-decoration: underline !important;
  opacity: 1 !important;
}

Scoping it with JustZix

Some of these rules are aggressive — blocking sub-second intervals could affect a live-updating widget you actually want. So scope them. Create a folder called "Anti dark-patterns" and add one rule per shopping site you use, each with a tight URL pattern. Toggle the whole folder off when you are not shopping. You get a calmer checkout without side effects elsewhere.

See also

Shopping should be a decision, not a reaction. Install JustZix, paste the CSS block above, and watch the manufactured pressure vanish from every product page you visit.

Rate this post

No ratings yet — be the first.

Try it yourself

Install JustZix and paste any snippet from this article. Two minutes from zero to a working rule across all your devices.

Get JustZix

Features · How it works · Examples · Use cases