DEV Community

Cover image for I built a Chrome extension that catches every dark pattern trick on shopping sites. Here's exactly how.
carlos lopez
carlos lopez

Posted on

I built a Chrome extension that catches every dark pattern trick on shopping sites. Here's exactly how.

A few months ago I was about to buy a flight. The page showed "Only 2 seats left at this price" in red letters. I hesitated, then refreshed the page out of curiosity.

The counter said "Only 2 seats left" again. Same number. It had been resetting on every page load the whole time.

That's when I started cataloguing every trick I'd seen on shopping sites and built a Chrome extension that flags them automatically, in real time, on any page.

This isn't just my opinion — it's documented research

In 2019, Princeton and University of Chicago researchers scraped 11,000 shopping sites and found dark patterns on more than 1,250 of them. The FTC has since fined several companies specifically for fake countdown timers and pre-checked subscription boxes. This isn't a grey area anymore — it's a known, studied, and increasingly regulated practice.

The extension targets four categories that account for most of what's out there.

The four patterns

Fake urgency. Countdown timers that reset, "X people are viewing this" badges that never change, "only N left in stock" that's been static for a week.

Trap checkboxes. A checkbox for "Yes, sign me up for the newsletter" that's pre-checked and styled to blend into the page so you don't notice it.

Confirmshaming. The decline button reads "No thanks, I don't want to save money" instead of just "No thanks."

Psychological pricing. Prices ending in .99 or .95 designed to register as a lower price bracket than they are.

Why no AI this time

My phishing detector used Claude because language and intent are genuinely ambiguous — you need a model that understands context. Dark patterns are different. They're structural. A countdown timer either resets on reload or it doesn't. A checkbox is either pre-checked or it isn't. That's a DOM query, not a judgment call.

So this one runs entirely on regex and DOM inspection. No API calls, no latency, no cost per scan, works offline. Sometimes the boring solution is the correct one.

Detecting the urgency pattern

The core check looks for known urgency phrases and cross-references them against actual page state:

const URGENCY_PHRASES = [
/only \d+ left/i,
/\d+ people (are )?(viewing|watching)/i,
/offer ends in/i,
/flash sale/i,
/limited time/i
];

function isLikelyFake(element) {
const text = element.textContent;
const matchesPattern = URGENCY_PHRASES.some(p => p.test(text));
if (!matchesPattern) return false;

// Countdown timers that are actually fake almost always
// reset to the same value across page loads — checked via
// localStorage comparison against the previous visit
const stored = localStorage.getItem('dpd_seen_' + location.pathname);
const isStatic = stored === text;
localStorage.setItem('dpd_seen_' + location.pathname, text);

return matchesPattern && isStatic;
}

The trick isn't the regex — it's storing what you saw on the last visit and comparing. A genuine "3 left in stock" notice changes over time. A fake one doesn't.

Detecting trap checkboxes

This one's simpler — just check if a checkbox tied to subscription/marketing language defaults to checked:

const TRAP_KEYWORDS = /newsletter|subscribe|offers|insurance|protection plan/i;

function findTrapCheckboxes() {
return [...document.querySelectorAll('input[type="checkbox"]')]
.filter(cb => cb.checked && TRAP_KEYWORDS.test(getLabelText(cb)));
}

The hard part isn't the logic, it's getLabelText() — checkbox labels are wrapped, nested, or associated via for= attributes in about six different inconsistent ways depending on the site. That function ended up being three times longer than the detection logic itself.

Marking it on the page

Detection is half the work. The other half is surfacing it without breaking the page layout:

function markElement(el, label) {
const badge = document.createElement('span');
badge.textContent = '⚠ ' + label;
badge.style.cssText =
position: absolute; background: #ef4444; color: white;
font-size: 11px; padding: 2px 8px; border-radius: 10px;
z-index: 999999; pointer-events: none;
;
el.style.position = 'relative';
el.appendChild(badge);
}

Catching dynamically injected patterns

Plenty of these patterns get injected after the initial page load — countdown widgets, exit-intent popups with fake scarcity. A one-time scan on page load misses them.

const observer = new MutationObserver(() => scanPage());
observer.observe(document.body, { childList: true, subtree: true });

This runs the same detection logic on every DOM mutation, debounced to avoid hammering the page on sites with constant re-renders (looking at you, infinite scroll feeds).

Results

I ran it against 30 ecommerce sites I had open in old tabs — a mix of fast fashion, flight booking, and a few subscription services. It flagged something on 19 of them. Two false positives: a genuinely real "low stock" indicator on a small print-on-demand shop, and a checkbox for "remember me" that the keyword filter misread as marketing consent.

Everything else held up. The fake countdown timers in particular were depressingly consistent — same number, every single reload.

FAQ

Is this legal to build and use?
Yes. You're reading and analyzing the public DOM of a page you're voluntarily visiting in your own browser. No data leaves your machine.

Does it slow down the pages I visit?
Not noticeably. The regex checks are cheap and the MutationObserver is debounced.

Can I use this in a commercial product?
The source is MIT licensed in the free version. Build on it.

Why not just block the patterns instead of flagging them?
Some checkboxes are legitimately useful (extended warranty you actually want). The goal is informed choice, not removing functionality you might want.


Full source code: https://carlosdevlop.gumroad.com/l/dark-patterns-detector

Top comments (0)