GDPR Art. 7 and Art. 4(11) define valid consent as freely given, specific, informed, and unambiguous — requiring an affirmative act. Pre-ticked boxes are explicitly prohibited. A cookie banner with a single 'Accept All' button and no per-category breakdown fails both the granularity requirement and the specificity requirement: users cannot meaningfully consent to 'analytics' and 'advertising' as a single undifferentiated mass. The ePrivacy Directive Art. 5(3) separately requires consent before setting non-essential cookies. Analytics scripts that fire unconditionally on page load — before the user has interacted with any banner — are violating both frameworks simultaneously, and this is detectable by any regulator with a browser dev tools panel.
Critical because pre-ticked non-essential cookie categories and unconditional script loading constitute unlawful processing under GDPR Art. 6(1)(a) — consent is the claimed basis but was never validly obtained.
Implement a consent banner with separate, independently toggleable categories, all defaulting to false. Conditional script loading must gate on the stored consent value.
// components/ConsentBanner.tsx — key structural requirements
const [choices, setChoices] = useState<ConsentChoices>({
analytics: false, // MUST default to false — Art. 7 requires affirmative opt-in
marketing: false,
})
// Non-essential scripts must not load until consent is granted:
function applyConsent(choices: ConsentChoices) {
if (choices.analytics) {
// dynamically load analytics script only after explicit consent
loadScript('https://www.googletagmanager.com/gtag/js?id=GA_ID')
}
}
Ensure users can 'Reject all non-essential' in a single click — the same effort as accepting. In Next.js, use next/script with strategy="lazyOnload" and only render it after consent. Never use strategy="beforeInteractive" or strategy="afterInteractive" for non-essential scripts.
ID: gdpr-readiness.consent-management.granular-opt-in
Severity: critical
What to look for: Look for a cookie consent banner or GDPR consent notice that appears on initial page load for new visitors. Search for component names like CookieBanner, ConsentBanner, CookieConsent, GDPRBanner, ConsentManager. Check the root layout file (layout.tsx, _app.tsx, App.vue, +layout.svelte) for consent component imports. Inspect the banner component: does it present separate toggles or checkboxes for different cookie purposes (at minimum: necessary/essential vs. analytics vs. marketing/advertising)? Verify that non-essential categories are NOT pre-checked — GDPR opt-in requires explicit, affirmative user action. Check whether analytics and tracking scripts load conditionally based on consent state, or whether they fire unconditionally on page load. Count every consent purpose presented to the user (analytics, marketing, personalization, etc.) and enumerate whether each is independently toggleable. Before evaluating, extract and quote the exact consent prompt text shown to users — the banner, modal, or checkbox text that obtains consent.
Pass criteria: A consent notice appears on first load for new visitors (no prior consent stored). It presents at least two categories (essential vs. non-essential) with separate toggles. All non-essential categories default to off/unchecked. Users can reject all non-essential cookies in one click. Non-essential scripts (analytics, tracking pixels, advertising) only load after the user grants consent for that purpose. At least 1 implementation must be confirmed.
Fail criteria: No consent banner on first load. Banner pre-checks non-essential categories. No per-category granularity — single accept/reject with no toggle. Non-essential scripts load before consent is given. Cookie wall that gates access to the service behind accepting cookies. Do NOT pass if a cookie consent banner exists but non-essential cookies or tracking scripts load before the user interacts with the banner.
Skip (N/A) when: Application is a purely static site with no cookies, analytics, user authentication, or any third-party scripts that set cookies.
Detail on fail: Specify exactly what is wrong. Example: "No consent banner found. Google Analytics and Facebook Pixel load unconditionally on every page." or "Consent banner has a single 'Accept Cookies' button with no per-category granularity and no reject option." or "Analytics and marketing consent categories are pre-ticked. User must actively uncheck to opt out — GDPR requires opt-in, not opt-out.".
Remediation: Implement a GDPR-compliant consent banner with per-purpose granularity:
// components/ConsentBanner.tsx
'use client'
import { useState, useEffect } from 'react'
type ConsentChoices = { analytics: boolean; marketing: boolean }
const CONSENT_KEY = 'gdpr_consent_v1'
export function ConsentBanner() {
const [visible, setVisible] = useState(false)
const [choices, setChoices] = useState<ConsentChoices>({
analytics: false, // MUST default to false
marketing: false, // MUST default to false
})
useEffect(() => {
if (!localStorage.getItem(CONSENT_KEY)) setVisible(true)
}, [])
function save(accepted: ConsentChoices) {
const record = {
choices: accepted,
timestamp: new Date().toISOString(),
consentVersion: '2026-02-01',
}
localStorage.setItem(CONSENT_KEY, JSON.stringify(record))
setVisible(false)
applyConsent(accepted)
}
if (!visible) return null
return (
<div className="fixed bottom-0 left-0 right-0 z-50 p-4 bg-white border-t shadow-xl">
<p className="mb-3 text-sm">
We use essential cookies to run this site. With your permission, we also use
optional cookies to understand usage and show relevant content.
</p>
<div className="flex flex-col gap-2 mb-4">
<label className="flex items-center gap-2 text-sm opacity-50">
<input type="checkbox" checked disabled />
Essential (always on — required for the site to function)
</label>
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={choices.analytics}
onChange={e => setChoices(c => ({ ...c, analytics: e.target.checked }))}
/>
Analytics — helps us understand how the site is used
</label>
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={choices.marketing}
onChange={e => setChoices(c => ({ ...c, marketing: e.target.checked }))}
/>
Marketing — personalised content and ads
</label>
</div>
<div className="flex gap-2">
<button onClick={() => save({ analytics: true, marketing: true })}>
Accept all
</button>
<button onClick={() => save({ analytics: false, marketing: false })}>
Reject non-essential
</button>
<button onClick={() => save(choices)}>Save preferences</button>
</div>
</div>
)
}
function applyConsent(choices: ConsentChoices) {
window.gtag?.('consent', 'update', {
analytics_storage: choices.analytics ? 'granted' : 'denied',
ad_storage: choices.marketing ? 'granted' : 'denied',
})
}