DEV Community

Cover image for Cookie Banners Are Tiny Applications: What Shadow DOM and Iframes Taught Me About Consent Automation
GetPawsOff
GetPawsOff

Posted on

Cookie Banners Are Tiny Applications: What Shadow DOM and Iframes Taught Me About Consent Automation

Most people think cookie banners are simple.

Click Accept. Click Reject. Move on.

After building a browser extension that automatically rejects cookie banners across a wide range of sites, I learned they're anything but simple. Here's what actually surprised me.

1. The "Reject" button often isn't hidden - it's relocated, or it isn't a button at all

Consent Management Platforms (CMPs) like OneTrust, Sourcepoint, Didomi, and Cookiebot don't all render the same way, and some make automation genuinely hard:

  • OneTrust usually gives you real DOM nodes with stable-ish IDs (#onetrust-reject-all-handler), but the wording and nesting shift between versions.
  • Sourcepoint frequently renders inside a cross-origin iframe, so you can't just querySelector from the parent page - a content script has to inject into that frame directly and coordinate with the parent over postMessage.
  • Didomi and some Cookiebot deployments use Shadow DOM, so a plain document.querySelector walks right past the controls unless you pierce every shadowRoot.
  • Some CMPs don't create new elements when a banner "appears" - they flip an existing element's aria-hidden or CSS class from hidden to visible. If your code only watches for new nodes being added, you'll miss it entirely.

That last one is the trap. A MutationObserver configured for childList alone will sail past a banner that was in the DOM the whole time:

const observer = new MutationObserver((mutations) => {
  for (const m of mutations) {
    if (m.type === 'attributes' &&
        (m.attributeName === 'aria-hidden' || m.attributeName === 'class')) {
      maybeCheckForConsentBanner(m.target);
    }
  }
});

observer.observe(document.documentElement, {
  attributes: true,
  attributeFilter: ['aria-hidden', 'class', 'style'],
  subtree: true,
});
Enter fullscreen mode Exit fullscreen mode

You need both childList and attributes observation, plus a recursive shadow-root walk, or you'll silently fail on a meaningful slice of sites.

I lost an evening to exactly this before I understood it. A banner I could see with my own eyes wasn't triggering my detector at all - no console errors or missed selector, nothing. I'd wired the observer for childList only, so it was watching for new nodes on a site that never added any. The banner had been sitting in the DOM since page load with aria-hidden="true"; the site just flipped that one attribute to show it. From the observer's point of view, nothing had happened.

2. Rejecting the cookies isn't the same as fixing the page

Early on I made this mistake: click "Reject," everything looks successful except the page can't scroll anymore.

Many CMPs drop a full-page backdrop (position: fixed, high z-index) over the site while the banner is open, and set overflow: hidden on <body> or <html> to lock scrolling. If you dismiss the banner but don't clean up the backdrop and restore the original overflow value, the user is left with a "privacy-respecting" page that's completely unusable. Rejection is a two-part job: click the right button, then verify the page actually returns to a normal, scrollable state.

3. Selectors rot - this is a maintenance project, not a one-time build

"Why not just write one rule per site?" Because next month's redesign breaks it. Consent UIs get A/B tested, rebranded, and re-skinned constantly, since acceptance rate is a real KPI for the companies selling these CMPs. A selector that works today can be dead in a week.

The practical fix isn't cleverer selectors. Detect "a modal-like overlay just appeared with consent-flavored text" generically, then apply a ranked list of reject strategies (known selectors first, generic heuristics like "smallest/leftmost button in the dialog" as fallback). When a specific rule breaks, only that rule needs fixing the detection layer keeps working.

4. Deterministic rules beat "AI-powered" for this problem

There's pressure to slap "AI-powered" on everything right now. I didn't, on purpose. For clicking buttons on other people's websites, deterministic rules have real advantages:

  • Predictable - the same input always produces the same action.
  • Fast - no inference latency on every page load.
  • Inspectable - you can read exactly what a rule does before it runs.
  • Debuggable - when a rule fails, you know precisely why, and you can fix that one rule without retraining anything.

A rules engine that's wrong is a bug. A model that's wrong is a mystery and on a security-adjacent surface (clicking buttons based on page content), "mystery" is not a property you want.

5. The blocking logic was the easy part

The harder problem turned out to be trust, not code. Plenty of people already want to reject cookies by default - what they don't know is whether the tool doing it on their behalf is trustworthy. An extension that touches every page you visit is exactly the kind of software that deserves scrutiny before you install it, not after.

That reframed the project for me. Writing a rule that correctly identifies and clicks a reject button in a Shadow DOM three levels deep is a fun problem. Explaining, clearly and verifiably, what the software does and doesn't do with your data is the actual product.

Final thought

Cookie banners aren't just annoying popups. They're small, adversarially-tuned applications, often built to make acceptance easy and rejection hard. Writing software that navigates them reliably across iframes, Shadow DOM, attribute-only state changes, and constant redesigns turned out to be a far more interesting engineering problem than "just click the button" ever suggested.

Have you run into a CMP that behaves differently from any of this? I'd like to hear about it every strange implementation is one more edge case that makes the rules more robust.

Top comments (0)