# Banner offers accept-all, reject-all, and per-category granular controls

- **Pattern:** `ab-000797` (`cookie-consent-compliance.banner-ux.accept-reject-per-category`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000797
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000797)

## Why it matters

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).

## Severity rationale

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.

## Remediation

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.

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

## Detection

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

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

---

## External references

- gdpr Art. 7
- gdpr Recital 43
- eprivacy Art. 5(3)
- gdpr Art. 6(1)(a)

Taxons: privacy-consent, regulatory-conformance

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