# Cookie consent banner displays on first visit before non-essential cookies are set

- **Pattern:** `ab-000795` (`cookie-consent-compliance.banner-ux.banner-first-visit`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000795
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000795)

## Why it matters

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.

## Severity rationale

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.

## Remediation

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.

```tsx
// 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.

## Detection

- **ID:** `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:

  ```tsx
  // 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>
    )
  }
  ```

  ```tsx
  // 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.
---

## External references

- eprivacy Art. 5(3)
- gdpr Art. 7
- ccpa §1798.135

Taxons: privacy-consent, regulatory-conformance

HTML version: https://auditbuffet.com/patterns/ab-000795
