GDPR Recital 43 states that consent is not freely given when the data subject cannot refuse without detriment, and GDPR Art. 7 requires withdrawal to be as easy as giving consent. A banner with only an 'Accept All' button — no per-category controls, no reject path — is an unlawful consent mechanism regardless of its visual polish. Regulators treat accept-only banners as no consent at all, meaning every cookie set under them lacks lawful basis under both GDPR Art. 6(1)(a) and ePrivacy Art. 5(3).
High because a banner lacking a reject path or per-category controls is treated by EU regulators as an unlawful consent mechanism, invalidating all non-essential cookies collected under it.
Implement a three-path banner: Accept All, Reject All, and a per-category preference panel. All three must be reachable from the initial banner without extra navigational steps beyond opening the preferences panel.
export function CookieBanner({ onSave }: { onSave: (state: ConsentState) => void }) {
const [showDetails, setShowDetails] = useState(false)
const [analytics, setAnalytics] = useState(false)
const [marketing, setMarketing] = useState(false)
return (
<div role="dialog" aria-label="Cookie consent">
{showDetails && (
<fieldset>
<legend>Manage preferences</legend>
<label>
<input type="checkbox" checked={analytics}
onChange={e => setAnalytics(e.target.checked)} />
Analytics
</label>
<label>
<input type="checkbox" checked={marketing}
onChange={e => setMarketing(e.target.checked)} />
Marketing
</label>
</fieldset>
)}
<button onClick={() => onSave({ analytics: true, marketing: true })}>Accept all</button>
<button onClick={() => onSave({ analytics: false, marketing: false })}>Reject all</button>
<button onClick={() => showDetails ? onSave({ analytics, marketing }) : setShowDetails(true)}>
{showDetails ? 'Save preferences' : 'Manage preferences'}
</button>
</div>
)
}
ID: cookie-consent-compliance.banner-ux.accept-reject-per-category
Severity: high
What to look for: Read the consent banner component's rendered JSX/HTML. Look for three distinct interactive elements: (1) an "Accept all" button or equivalent that sets all categories to true, (2) a "Reject all" or "Decline all" button or equivalent that sets all non-essential categories to false, and (3) per-category checkboxes or toggles that let users independently enable analytics without enabling marketing, for example. Many AI-generated banners include only a single "Accept" button with no reject option, or a two-button "Accept/Decline" with no per-category granularity. Check whether clicking "Accept all" and then "Cookie Preferences" reflects the correct saved state. Check whether clicking "Reject all" actually clears or prevents loading of all non-essential scripts, not just records the preference.
Pass criteria: Count all cookie categories offered in the consent UI. The banner provides: (1) an Accept All option that accepts all non-essential categories, (2) a Reject All or Save with all unchecked option that declines all non-essential categories, and (3) per-category granular toggles so a user can accept analytics but not marketing. All three paths result in the correct script loading behavior. At least 2 category-level controls must be available (e.g., Analytics, Marketing).
Fail criteria: Banner shows only a single "Accept" or "OK" button with no way to decline. Banner has Accept/Decline but no per-category breakdown. Per-category toggles exist visually but selecting them has no effect on which scripts load.
Skip (N/A) when: No consent banner exists (already failing at banner-first-visit). Application only has essential cookies — no non-essential categories to toggle.
Detail on fail: Specify what is missing. Example: "Consent banner has Accept button only. No reject option and no per-category toggles." or "Banner has Accept All and Reject All buttons but no per-category granularity. Users cannot accept analytics while declining marketing.".
Remediation: Implement a three-path banner. A cookie settings drawer or modal is an accepted pattern for the granular view:
// Minimal three-path banner structure
export function CookieBanner({ onSave }: { onSave: (state: ConsentState) => void }) {
const [showDetails, setShowDetails] = useState(false)
const [analytics, setAnalytics] = useState(false)
const [marketing, setMarketing] = useState(false)
return (
<div role="dialog" aria-label="Cookie consent">
<p>We use cookies to improve your experience. Choose your preferences below.</p>
{showDetails && (
<fieldset>
<legend>Cookie categories</legend>
<label>
<input type="checkbox" disabled checked readOnly />
Strictly necessary (always on)
</label>
<label>
<input type="checkbox" checked={analytics}
onChange={e => setAnalytics(e.target.checked)} />
Analytics — helps us understand how the site is used
</label>
<label>
<input type="checkbox" checked={marketing}
onChange={e => setMarketing(e.target.checked)} />
Marketing — enables personalized ads
</label>
</fieldset>
)}
<button onClick={() => onSave({ analytics: true, marketing: true })}>
Accept all
</button>
<button onClick={() => onSave({ analytics: false, marketing: false })}>
Reject all
</button>
<button onClick={() => {
if (showDetails) onSave({ analytics, marketing })
else setShowDetails(true)
}}>
{showDetails ? 'Save preferences' : 'Manage preferences'}
</button>
</div>
)
}