Skip to main content
CROSHOPS

All insights

What happens when a visitor declines cookies in your Shopify store

· 11 min read

We clicked Decline on a live EU Shopify store's cookie banner. App pixels went quiet; YouTube, chat, analytics and fonts kept loading.

A visitor clicks Decline on your cookie banner. You'd expect the store to go quiet. On a live EU Shopify store we work on, it didn't.

Shopify's built-in cookie banner with its default text and three buttons: Manage preferences, Accept, Decline
The banner we tested: Shopify's built-in cookie banner with its default text. Screenshot, October 2026.

We tested it with three fresh browser profiles from Poland: no choice, Decline, Accept. Each time we logged every request, cookie and storage key on the homepage and a product page.

The apps behaved. The theme did not. Every pixel installed through a Shopify app waited for Accept. Four things in the theme and in app scripts kept loading after Decline, and before any choice at all.

After Decline: app pixels sent 0 requests, while YouTube embeds, a chat widget, a web analytics script and popup fonts still loaded
Recreated from our test on one live EU Shopify store, October 2026.

Below are those four leaks in that order, plus a fifth that broke a form. Each one: the problem, how we found it, the fix, the result. At the end, the same check as a list you can run on your own store.

This is a technical check, not legal advice. What your banner must block is a question for your lawyer. What it actually blocks, you can see for yourself.

Why the banner missed them: it only stops code that asks

Shopify's banner stores the visitor's choice in the Customer Privacy API. Shopify's own tools read that choice. So do app pixels and custom pixels that declare a privacy permission (Settings > Customer events). That's why Google, Meta, TikTok, the email tool's tracking and an affiliate pixel all stayed silent until Accept (two of them by their own code, see below).

Code in the theme doesn't ask unless someone built it to. A script pasted into the layout, a YouTube iframe inside a metafield, or a widget injected by an app embed runs whatever the visitor chose. Shopify's help says as much: third-party cookies or pixels added by hand or through apps may need a third-party banner or custom logic to respect consent.

Shopify banner flow: app and custom pixels are blocked after Decline; theme scripts and app embeds still run unless they check consent

So the banner isn't broken. It just can't see everything. Here's what it couldn't see on this store.

Leak 1: YouTube embeds loaded the player and Google's ad-ID calls

Problem. One product page carried 22 YouTube iframes from four sources: a video modal, product-tab HTML stored in a metafield, video reviews and deferred media. After Decline, 6 players still loaded: 34 requests, 5 youtube.com cookies and calls to Google's doubleclick ad-ID endpoint.

How we found it. Network tab, filter youtube|doubleclick, click Decline, reload. Any row in that list is a fail.

Chrome DevTools Network tab filtered for youtube and doubleclick: fail shows YouTube and ad-ID requests after Decline, pass shows no requests
Recreated from our test on one live EU Shopify store, October 2026.

Fix. Each iframe became a click-to-play placeholder. The video loads only after the visitor clicks play or accepts cookies.

Result. The same page after Decline: 22 placeholders, 0 YouTube requests, 0 cookies.

Click-to-play placeholder: 34 YouTube requests and 5 cookies after Decline before the fix, 0 and 0 after
Recreated from our test on one live EU Shopify store, October 2026.

Do it yourself. Paste this where the video should sit (a Custom Liquid block or the page's HTML) in place of the YouTube iframe, and replace VIDEO_ID with the ID from the video's link:

<button type="button" class="yt-play" data-yt="VIDEO_ID" style="width:100%;aspect-ratio:16/9">▶ Play video (loads YouTube)</button>
<script>
  function playYouTube(btn, autoplay) {
    var f = document.createElement('iframe'); f.title = 'YouTube video'; f.allow = 'autoplay; fullscreen';
    f.src = 'https://www.youtube-nocookie.com/embed/' + btn.dataset.yt + (autoplay ? '?autoplay=1' : '');
    f.style.cssText = 'width:100%;aspect-ratio:16/9;border:0'; btn.replaceWith(f);
  }
  function videosIfAllowed() { var p = window.Shopify.customerPrivacy; if (p && p.marketingAllowed()) document.querySelectorAll('.yt-play').forEach(function (b) { playYouTube(b, false); }); }
  document.addEventListener('click', function (e) { var b = e.target.closest('.yt-play'); if (b) playYouTube(b, true); });
  window.Shopify.loadFeatures([{ name: 'consent-tracking-api', version: '0.1' }], videosIfAllowed);
  document.addEventListener('visitorConsentCollected', videosIfAllowed);
</script>

Nothing loads from YouTube until the visitor clicks play or accepts marketing cookies. After Accept, every video on the page loads at once; our store fix waits until each one scrolls into view, which takes more code. youtube-nocookie.com is YouTube's privacy-enhanced mode (opens in a new tab). It changes how views are used, not whether consent is needed, so keep the click.

Leak 2: the chat widget tracked every page view

Problem. A Zendesk-type chat widget loaded on every page. It stored a persistent device ID in local storage and sent a page-view event (URL, title, referrer) to the chat vendor on every page, whatever the visitor chose.

How we found it. Filter Network by the chat vendor's domain, then open Application > Local storage.

Fix. The widget now loads when the visitor clicks the chat launcher, or after consent.

Result. After Decline, only the launcher loads: no vendor requests and no device ID. One click still opens the chat.

Do it yourself. Paste this in layout/theme.liquid before </body> in place of the widget's own script tag, and replace CHAT_SCRIPT_URL with your widget's script URL (including your widget key):

<button type="button" id="chat-launcher">Chat with us</button>
<script>
  function loadChat(open) {
    if (window.chatLoaded) return; window.chatLoaded = true;
    var s = document.createElement('script');
    s.async = true; s.src = 'CHAT_SCRIPT_URL';
    document.head.appendChild(s).onload = function () { document.getElementById('chat-launcher').remove(); if (open) { /* your widget's open call */ } };
  }
  function chatIfAllowed() { var p = window.Shopify.customerPrivacy; if (p && p.analyticsProcessingAllowed()) loadChat(false); }
  document.getElementById('chat-launcher').addEventListener('click', function () { loadChat(true); });
  window.Shopify.loadFeatures([{ name: 'consent-tracking-api', version: '0.1' }], chatIfAllowed);
  document.addEventListener('visitorConsentCollected', chatIfAllowed);
</script>

Style the button like your chat bubble. Replace the comment with your widget's open method (every chat tool documents one), so one click both loads and opens the chat. If your policy lists the chat under another purpose, swap analyticsProcessingAllowed for preferencesProcessingAllowed or marketingAllowed.

Leak 3: an analytics script was hard-coded in the theme head

Problem. An Ahrefs-type web analytics script sat directly in the theme's head, so it ran for everyone.

How we found it. In the theme code editor, search the layout file and snippets for <script src= pointing to third-party domains.

Fix. The script is now injected only after analytics consent.

Result. Not loaded before a choice or after Decline.

Do it yourself. Paste this in layout/theme.liquid before </head>, in place of the old <script src=…> tag, and replace the URL with your script's:

<script>
  function loadVendorIfAllowed() {
    var privacy = window.Shopify.customerPrivacy;
    if (window.vendorLoaded || !privacy || !privacy.analyticsProcessingAllowed()) return;
    window.vendorLoaded = true;
    var s = document.createElement('script');
    s.async = true; s.src = 'https://vendor.example/script.js';
    document.head.appendChild(s);
  }
  window.Shopify.loadFeatures([{ name: 'consent-tracking-api', version: '0.1' }], loadVendorIfAllowed);
  document.addEventListener('visitorConsentCollected', loadVendorIfAllowed);
</script>

It checks consent on page load and again when the visitor makes a choice, and loads the script once. For an ad tool, swap analyticsProcessingAllowed for marketingAllowed. Use a different vendorLoaded name for each script.

Names from Shopify's Customer Privacy API (opens in a new tab). The docs show version: '0.2', but on the theme stores we checked, Shopify's own banner had already loaded 0.1 and a 0.2 request returned an error. That's why the snippet reads Shopify.customerPrivacy directly instead of trusting the callback. The same pattern went live on a client store this week.

Leak 4: fonts came in through the email popup, not the theme

Problem. The theme already self-hosted its font. But the email sign-up tool (Klaviyo-type) injected Google Fonts and an Adobe Fonts kit on every page, before consent. A back-in-stock modal also imported a Google font when it opened.

How we found it. Filter Network for fonts.googleapis, fonts.gstatic and typekit. Open the popup and the back-in-stock modal too.

Fix. The font files went onto the shop's own CDN, and the popup tool was switched to that copy.

Result. Zero third-party font requests before consent.

Fonts have been tested in court at least once. On 20 January 2022 the Munich Regional Court ruled that a site loading Google Fonts from Google's servers without consent had passed the visitor's IP address to Google, and awarded €100 in damages. That is one court's view, not EU law.

Leak 5: an old ad call broke a form

Problem. A theme contact form still called another region's Google Ads account. With consent declined, gtag isn't defined, so the call threw an error and the form code after it never ran.

How we found it. Search the theme for gtag( and conversion, then submit the form with the console open.

Fix. The call was removed.

Result. The form code runs again, with no console error.

None of these five needed a new app. Each was one change in the theme or in the popup tool's settings.

The Google tags were never the problem. The store runs Google Consent Mode v2 in basic mode, and it behaved as documented: no Google tags before a choice or after Decline, and after Accept all four signals granted (ad_storage, analytics_storage, ad_user_data, ad_personalization).

Basic isn't the only option. Advanced mode loads the tags with defaults set to denied and sends cookieless pings after Decline. Google uses those pings for an advertiser-specific model instead of a general one. Which mode you run is a legal and business decision, not a technical one.

Consent Mode v2: basic sends nothing before a choice or after Decline; advanced sends cookieless pings; both send full tags after Accept
Source: Google Ads Help, About consent mode.

Checkout followed the same choice. A buyer who declined on the storefront sent no purchase hit to Google.

Two app pixels declared no privacy permission, so Shopify couldn't gate them. They gated themselves and stayed silent until Accept. You only know that by testing.

Run the same check on your store in 10 minutes

Set up once: a fresh private window, an EU location or VPN, and dev tools open on Network (Preserve log on, cache off) and Application (Cookies, Local storage). Use a product page with video and reviews, not only the homepage. Run the list three times: no choice, Decline, Accept.

  1. Video embeds. Filter youtube|doubleclick. Pass: no requests and no youtube.com cookies before Accept.
  2. Chat widget. Filter the vendor's domain, check Local storage. Pass: nothing until the launcher is clicked or consent is given.
  3. Theme scripts. Search the layout and snippets for third-party <script src=. Pass: each one waits for the right consent.
  4. Fonts. Filter fonts.googleapis, fonts.gstatic, typekit; open popups too. Pass: zero third-party font requests before consent.
  5. Old ad tags. Search for gtag( and conversion. Pass: no calls to accounts you don't run, no console errors on forms.
  6. Consent Mode. Google Tag Assistant, Consent tab. Pass (basic): no Google tags before a choice or after Decline; after Accept, the On-page Update column shows all four signals granted.
  7. Pixels. Settings > Customer events. Pass: every app and custom pixel requires permission (custom pixels: Permission set to Required), or you've tested that it stays silent after Decline.
  8. Your policy. Pass: everything that still loads after Decline is strictly necessary (store, cart, checkout, payment, security), and your privacy policy lists each one with its purpose.

A banner that looks right proves nothing

Shopify's built-in cookie banner: it looks the same whether or not the store's theme respects Decline
Same banner, same buttons. Only testing shows what it actually blocks.

Test it, don't trust it. Run the eight checks in all three states: no choice, Decline, Accept.

Fix one leak at a time, then rerun the checks, so you know which change worked.

After Decline, only what the store needs should load: store, cart, checkout, payment, security. Anything else you're unsure about goes to your lawyer.

Sources