<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: GetPawsOff</title>
    <description>The latest articles on DEV Community by GetPawsOff (@getpawsoff).</description>
    <link>https://dev.to/getpawsoff</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4024849%2F071490ca-6f39-457d-9c13-0f96fbb540b6.png</url>
      <title>DEV Community: GetPawsOff</title>
      <link>https://dev.to/getpawsoff</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/getpawsoff"/>
    <language>en</language>
    <item>
      <title>Cookie Banners Are Tiny Applications: What Shadow DOM and Iframes Taught Me About Consent Automation</title>
      <dc:creator>GetPawsOff</dc:creator>
      <pubDate>Sat, 11 Jul 2026 07:44:58 +0000</pubDate>
      <link>https://dev.to/getpawsoff/cookie-banners-are-tiny-applications-what-shadow-dom-and-iframes-taught-me-about-consent-automation-4cm7</link>
      <guid>https://dev.to/getpawsoff/cookie-banners-are-tiny-applications-what-shadow-dom-and-iframes-taught-me-about-consent-automation-4cm7</guid>
      <description>&lt;p&gt;Most people think cookie banners are simple.&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Accept&lt;/strong&gt;. Click &lt;strong&gt;Reject&lt;/strong&gt;. &lt;strong&gt;Move on&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The "Reject" button often isn't hidden - it's relocated, or it isn't a button at all
&lt;/h2&gt;

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

&lt;ul&gt;
&lt;li&gt;OneTrust usually gives you real DOM nodes with stable-ish IDs (&lt;code&gt;#onetrust-reject-all-handler&lt;/code&gt;), but the wording and nesting shift between versions.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;Didomi and some Cookiebot deployments use Shadow DOM, so a plain &lt;code&gt;document.querySelector&lt;/code&gt; walks right past the controls unless you pierce every &lt;code&gt;shadowRoot&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Some CMPs don't create new elements when a banner "appears" - they flip an existing element's &lt;code&gt;aria-hidden&lt;/code&gt; or CSS class from &lt;code&gt;hidden&lt;/code&gt; to &lt;code&gt;visible&lt;/code&gt;. If your code only watches for new nodes being added, you'll miss it entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the trap. A &lt;code&gt;MutationObserver&lt;/code&gt; configured for &lt;code&gt;childList&lt;/code&gt; alone will sail past a banner that was in the DOM the whole time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MutationObserver&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;mutations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;mutations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;attributes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attributeName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aria-hidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attributeName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;class&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;maybeCheckForConsentBanner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;attributeFilter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aria-hidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;class&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;style&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;subtree&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need both &lt;code&gt;childList&lt;/code&gt; and &lt;code&gt;attributes&lt;/code&gt; observation, plus a recursive shadow-root walk, or you'll silently fail on a meaningful slice of sites.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;childList&lt;/code&gt; 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 &lt;code&gt;aria-hidden="true"&lt;/code&gt;; the site just flipped that one attribute to show it. From the observer's point of view, nothing had happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Rejecting the cookies isn't the same as fixing the page
&lt;/h2&gt;

&lt;p&gt;Early on I made this mistake: click "Reject," everything looks successful except the page can't scroll anymore.&lt;/p&gt;

&lt;p&gt;Many CMPs drop a full-page backdrop (&lt;code&gt;position: fixed&lt;/code&gt;, high &lt;code&gt;z-index&lt;/code&gt;) over the site while the banner is open, and set &lt;code&gt;overflow: hidden&lt;/code&gt; on &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; 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. &lt;strong&gt;Rejection is a two-part job&lt;/strong&gt;: click the right button, then verify the page actually returns to a normal, scrollable state.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Selectors rot - this is a maintenance project, not a one-time build
&lt;/h2&gt;

&lt;p&gt;"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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Deterministic rules beat "AI-powered" for this problem
&lt;/h2&gt;

&lt;p&gt;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:&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The blocking logic was the easy part
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>extensions</category>
      <category>googlechrome</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
