ePrivacy Art. 5(3) prohibits storing information on a user's device without prior informed consent. When analytics and ad pixels fire on page load before a consent banner appears, every first visit becomes an unlawful data collection event. Regulators — the CNIL, ICO, and Austrian DSB — have each issued fines specifically for this ordering failure, not for lacking a banner entirely. The business risk is not theoretical: a single DPA complaint can trigger an investigation across an entire platform.
Critical because a mis-ordered load sequence violates ePrivacy Art. 5(3) on every first page view, exposing the operator to per-visit regulatory liability before any user interaction occurs.
Render the consent component at the top of the root layout and move all non-essential scripts into its post-consent callback. In Next.js App Router, no <Script> tag for GA4, Facebook Pixel, or Hotjar should appear in app/layout.tsx unless it is inside a consent gate or using GA4 Consent Mode v2 with analytics_storage: 'denied' as the default.
// app/layout.tsx — correct ordering
import { CookieConsent } from '@/components/CookieConsent'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>{/* no third-party scripts here */}</head>
<body>
<CookieConsent /> {/* shows banner first; loads approved scripts after */}
{children}
</body>
</html>
)
}
The CookieConsent component reads localStorage.getItem('cookie_consent') in a useEffect; if nothing is stored, it shows the banner. Non-essential scripts are injected into the DOM only inside the loadApprovedScripts() callback that fires after the user saves a preference.
ID: cookie-consent-compliance.banner-ux.banner-first-visit
Severity: critical
What to look for: Read the root layout file (app/layout.tsx, pages/_document.tsx, +layout.svelte, index.html, etc.) and look at what executes on initial page load for a new visitor with no prior consent stored. Identify every script tag, <Script> component, or dynamic script loader that is present. Now check whether the consent banner component is also present in that layout. The consent banner must initialize and display before any non-essential script runs. Look for the ordering: does the consent check happen at the very top of the layout, or does it happen below GA4, Facebook Pixel, and Hotjar includes? In a well-implemented setup, non-essential scripts will be absent from the static layout and only injected into the DOM by the consent component after the user grants consent. Check the consent component's useEffect or onMounted — does it run localStorage.getItem('cookie_consent') and only show the banner when nothing is stored, confirming first-visit behavior?
Pass criteria: Count all consent banner trigger points. A consent banner component is present in the root layout. On first visit (no prior consent stored), the banner displays before any non-essential cookie is set. Non-essential third-party scripts (analytics, pixels, chat widgets) are either absent from the static layout or wrapped in a consent-first loading pattern that prevents them from running before the user makes a choice. At least 100% of first-visit scenarios must show the banner before any non-essential cookies are set.
Fail criteria: No consent banner found anywhere in the codebase or layout. Banner component exists but is never mounted in the root layout. Banner exists but analytics/pixel scripts load unconditionally in the <head> or body above the banner, meaning cookies are set before the user sees the banner.
Skip (N/A) when: The application sets no cookies at all and loads no third-party scripts — a purely static site with no analytics, no auth, no embedded content, and no third-party dependencies.
Detail on fail: Specify what is missing or mis-ordered. Example: "No consent banner component found. GA4 and Facebook Pixel load unconditionally in app/layout.tsx head." or "CookieBanner component exists in components/ but is not imported or rendered in the root layout." or "Banner component present but GA4 script tag appears above the banner in the layout — analytics fires before consent.".
Remediation: The core pattern is: render your consent banner first, load non-essential scripts second. In Next.js App Router:
// app/layout.tsx — correct consent-first structure
import { CookieConsent } from '@/components/CookieConsent'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
{/* NO third-party scripts here — they load conditionally after consent */}
</head>
<body>
<CookieConsent /> {/* renders banner on first visit; loads scripts after consent */}
{children}
</body>
</html>
)
}
// components/CookieConsent.tsx
'use client'
import { useEffect, useState } from 'react'
type ConsentState = { analytics: boolean; marketing: boolean }
export function CookieConsent() {
const [show, setShow] = useState(false)
const [consent, setConsent] = useState<ConsentState>({ analytics: false, marketing: false })
useEffect(() => {
const stored = localStorage.getItem('cookie_consent')
if (!stored) {
setShow(true) // first visit — show banner
} else {
const parsed: ConsentState = JSON.parse(stored)
loadApprovedScripts(parsed) // returning visitor — apply stored preferences
}
}, [])
function save(state: ConsentState) {
localStorage.setItem('cookie_consent', JSON.stringify({
...state,
timestamp: new Date().toISOString(),
version: '1', // bump when you add new third parties
}))
setShow(false)
loadApprovedScripts(state)
}
function loadApprovedScripts(state: ConsentState) {
if (state.analytics) {
// dynamically append GA4 script only after consent
const script = document.createElement('script')
script.src = 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXX'
script.async = true
document.head.appendChild(script)
}
}
if (!show) return null
return <div>{/* banner UI — see banner-accessible check for WCAG requirements */}</div>
}
If you prefer a managed consent platform, Cookiebot and Osano both handle the script-blocking pattern automatically: you mark non-essential <script> tags with type="text/plain" data-cookieconsent="statistics" and the platform activates them after consent.
Cross-reference: For broader privacy compliance and data protection requirements, see the GDPR Compliance audit.