Seitenbefund
  • Sample report
  • How it works
  • Blog
  • FAQ
  • For agencies
  • Pricing
EN/DEStart free website check
EN/DE
← Back to all posts
Legal·April 25, 2026·3 min read

Cookie banners without dark patterns

Three consent rules most banners break, the CJEU equivalence requirement, and a minimal pattern that avoids dark patterns.

by Lukas Freitag

A GDPR-compliant cookie banner is not a design problem. It is the correct application of three requirements: GDPR Art. 7 ("conditions for consent"), the ePrivacy Directive Art. 5(3), and — for German audiences — TTDSG §25. If your banner fails any of the three, the consent it stores is invalid, even if the user clicked "Accept".

The three failure modes audits keep finding

In ten audits we will find at least seven of the same pattern:

  1. "Accept all" prominent, "Reject" hidden. This violates the freely-given requirement. The CJEU ruling in Planet49 (Case C-673/17, October 2019) made it explicit: choices must be equivalent. A "Reject" button rendered as a footer link is not equivalent to a styled primary button.
  2. The close button counts as consent. Common in older implementations. No active expression of consent means no consent under GDPR.
  3. Tracking scripts load before the user picks. The banner is cosmetic. The damage is done. ePrivacy Art. 5(3) only permits storage without consent if it is "strictly necessary" — analytics never qualifies.

What the rules actually say

Three independent obligations, each enforceable on its own:

  • GDPR Art. 7(2): the request must be presented in an intelligible and easily accessible form, using clear and plain language.
  • GDPR Art. 7(3): withdrawing consent must be as easy as giving it. If accept is one click, reject must be one click.
  • ePrivacy Directive Art. 5(3) / TTDSG §25(1): storing or reading information on the user's device requires prior consent, except for cookies "strictly necessary" for a service the user explicitly requested (session, CSRF, language preference).

  • #gdpr
  • #cookies
  • #consent
  • #eprivacy
  • #legal

German version: cookie-banner-dsgvo-konform

Want to see where your site stands?

The free short check inspects visibility, structure, performance, and the technical baseline of your domain — passively, in minutes.

Start the short check

Related posts

  • SEO·April 29, 2026·3 min read

    Schema.org for small businesses

    Most small business sites need only three schema types. Here is what helps rich results and what we remove during audits.

    Read more →
  • Performance·April 18, 2026·4 min read

    Core Web Vitals explained

    What LCP, INP and CLS measure, why a Lighthouse 100 can still ship a slow site, and which fixes move most audits.

    Read more →
← Back to all posts
Seitenbefund

Passive website inspection for visibility, structure, performance, accessibility and baseline technical security.

Services

  • Penetration testing
  • SEO remediation
  • Accessibility remediation
  • Baseline security hardening

Legal

  • Imprint
  • Privacy
  • Terms

© 2026 Seitenbefund kontakt@seitenbefund.de

We only fetch publicly reachable pages. No logins, no port scans, no active security testing.

A minimal compliant pattern

The banner needs three buttons of equal visual weight: "Accept all", "Essential only", "Customize". No data processing fires before the user picks.

type Choice = 'all' | 'essential' | 'custom'

export function CookieBanner() {
  const [open, setOpen] = useState(true)
  const decide = (choice: Choice) => {
    setConsentCookie(choice)
    if (choice === 'all') loadAnalytics()
    setOpen(false)
  }

  if (!open) return null
  return (
    <div role="dialog" aria-labelledby="cookie-h">
      <h2 id="cookie-h">Cookie settings</h2>
      <p>We use cookies for statistical analysis.</p>
      <button type="button" onClick={() => decide('essential')}>
        Essential only
      </button>
      <button type="button" onClick={() => decide('custom')}>
        Customize
      </button>
      <button type="button" onClick={() => decide('all')}>
        Accept all
      </button>
    </div>
  )
}

Three points are deliberately load-bearing here:

  • loadAnalytics() runs only after the user clicks "Accept all". There is no code path that loads scripts before the decision.
  • The three buttons share DOM order and styling. Rendering "Accept all" as a <button> and "Reject" as an <a> link would already fail the equivalence test from Planet49.
  • role="dialog" plus aria-labelledby lets screen readers announce the banner as a modal rather than a passing announcement.

The withdrawal path: the forgotten third

Art. 7(3) says withdrawal must be as easy as giving consent. In practice that means a permanently visible "Cookie settings" link in the footer that re-opens the banner.

<button type="button" onClick={() => setBannerOpen(true)}>
  Cookie settings
</button>

Without this link, every banner is non-compliant — no matter how clean the initial flow.

Server logs are not analytics

Server-side IP logs kept for security and operations are permitted without consent under GDPR Recital 49. The same logs become analytics — and trigger Art. 5(3) — the moment they are forwarded to a tracking system.

The most common audit finding

In eight out of ten privacy audits, banners fail on point 3 from above: tracking scripts load before consent. The root cause is almost always the same — Google Tag Manager dropped into <head> without a consent gate, tags firing on page_view, the banner showing up afterward as cosmetics. The fix is one conditional in the GTM loader: consentState === 'all'.

The Seitenbefund short check passively records which hosts are contacted before consent and lists the offending requests so you can see the gap before the supervisory authority does.