JDCODE
← Startseite
Advanced Guide

Framer Cookie Manager Pro.

Setup GA4 & Meta Pixel mit einem DSGVO-konformen Slider-System.

Schritt 1: Code erstellen (CookieManager.tsx)

Erstelle ein neues Code-File in Framer (Assets > Code > New File), nenne es CookieManager.tsx und kopiere diesen vollständigen React-Code hinein.

import { useEffect, useState, MouseEvent, useCallback, useMemo } from "react"
import { addPropertyControls, ControlType } from "framer"

/**
 * 👻 PRIVACY SLIDER (Community Edition)
 * Ein "Privacy by Design" Experiment für Framer.
 * * ANLEITUNG:
 * 1. Ziehe die Komponente auf dein Overlay oder Footer.
 * 2. Sidebar: Trage deine GA4 & Pixel IDs ein.
 * 3. Sidebar: Setze die Links zu Datenschutz & Impressum.
 */

declare global {
    interface Window { fbq: any; _fbq: any; dataLayer: any; }
}

interface Props {
    googleAnalyticsID: string
    metaPixelID: string
    accentColor: string
    privacyLink: string
    imprintLink: string
    startClosed: boolean
}

export default function CookieManager(props: Props) {
    // Startet immer im Ghost Mode (Privacy by Default)
    const [sliderValue, setSliderValue] = useState(0)
    const [status, setStatus] = useState<"undecided" | "decided">("undecided")

    const modeConfig = useMemo(() => [
        {
            label: "Ghost Mode",
            desc: "Kein Tracking. Nur essenzielle Cookies.", 
            emoji: "👻",
            color: "#EFEBE5",
            textColor: "#000000",
            consent: "false",
        },
        {
            label: "Full Support",
            desc: "Aktiviert Google Analytics & Meta Pixel.", // Wichtig: Anbieter nennen!
            emoji: "❤️",
            color: props.accentColor,
            textColor: "#FFFFFF",
            consent: "true",
        }
    ], [props.accentColor])

    const current = modeConfig[sliderValue]

    const loadTrackingScripts = useCallback(() => {
        if (typeof window === "undefined") return
        
        // GA4 Logic
        if (props.googleAnalyticsID && !document.getElementById("ga-script")) {
            const s = document.createElement("script"); s.id = "ga-script"; s.async = true
            s.src = `https://www.googletagmanager.com/gtag/js?id=${props.googleAnalyticsID}`
            document.head.appendChild(s)
            window.dataLayer = window.dataLayer || []
            const gtag = (...args: any[]) => window.dataLayer.push(args)
            gtag("js", new Date()); gtag("config", props.googleAnalyticsID)
        }

        // Meta Pixel Logic
        if (props.metaPixelID && !document.getElementById("meta-pixel-marker")) {
            !(function(f:any,b:any,e:any,v:any,n:any,t:any,s:any){if(f.fbq)return;n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)})(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');
            window.fbq('init', props.metaPixelID); window.fbq('track', 'PageView')
            const m = document.createElement("div"); m.id = "meta-pixel-marker"; document.body.appendChild(m)
        }

        // Vercel Insights (Optionale Ergänzung)
        if (!document.querySelector('script[src*="/_vercel/insights/script.js"]')) {
            const vs = document.createElement("script"); vs.defer = true
            vs.src = "/_vercel/insights/script.js"; document.head.appendChild(vs)
        }
    }, [props.googleAnalyticsID, props.metaPixelID])

    useEffect(() => {
        const consent = localStorage.getItem("consent_granted")
        // Logik: Nur anzeigen, wenn noch nicht entschieden
        if (consent === "true") { setStatus("decided"); loadTrackingScripts() }
        else if (consent === "false") { setStatus("decided") }
        else { setStatus("undecided") }
    }, [loadTrackingScripts])

    const handleConfirm = (e: MouseEvent) => {
        e.preventDefault(); e.stopPropagation()
        localStorage.setItem("consent_granted", current.consent)
        setStatus("decided")
        
        if (current.consent === "true") { 
            requestAnimationFrame(() => setTimeout(loadTrackingScripts, 50)) 
        }
    }

    // Zeigt Komponente nur, wenn undecided ODER wenn im Editor "Start Closed" deaktiviert ist (zum Testen)
    if (status !== "undecided" && !props.startClosed) return null

    return (
        <div style={styles.container}>
            <style>{`
                .privacy-slider {
                    -webkit-appearance: none; width: 100%; height: 44px;
                    background: #F1F1F1; border-radius: 22px; outline: none;
                    padding: 0 6px; cursor: pointer; transition: background 0.3s ease;
                }
                .privacy-slider::-webkit-slider-thumb {
                    -webkit-appearance: none; width: 32px; height: 32px;
                    background: white; border-radius: 50%;
                    box-shadow: 0 4px 12px rgba(0,0,0,0.1);
                    transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
                }
                .privacy-slider::-webkit-slider-thumb:hover { transform: scale(1.1); }
            `}</style>

            <div style={styles.header}>
                <div style={{...styles.emojiBadge, background: sliderValue === 0 ? "#F8F8F8" : props.accentColor + "15"}}>
                    <span style={styles.emoji}>{current.emoji}</span>
                </div>
                <div style={styles.textContainer}>
                    <h4 style={styles.title}>{current.label}</h4>
                    <p style={styles.desc}>{current.desc}</p>
                </div>
            </div>

            <input 
                type="range" min="0" max="1" step="1" 
                value={sliderValue}
                onChange={(e) => setSliderValue(parseInt(e.target.value))}
                className="privacy-slider"
                style={{ background: sliderValue === 1 ? props.accentColor + "25" : "#E5E5E5" }}
            />

            <button
                onClick={handleConfirm}
                style={{ 
                    ...styles.button, 
                    background: current.color, 
                    color: current.textColor,
                    border: sliderValue === 0 ? "1px solid #E0DCD6" : "none"
                }}
            >
                Auswahl bestätigen
            </button>

            <div style={styles.footerLinks}>
                <a href={props.privacyLink} style={styles.link}>Datenschutz</a>
                <span style={{color: "#ccc"}}>•</span>
                <a href={props.imprintLink} style={styles.link}>Impressum</a>
            </div>
        </div>
    )
}

const styles: any = {
    container: {
        position: "fixed", bottom: "32px", right: "32px", 
        width: "90%", maxWidth: "320px", background: "#FFFFFF",
        borderRadius: "32px", boxShadow: "0 25px 60px rgba(0,0,0,0.2)",
        zIndex: 2147483647, padding: "24px", 
        fontFamily: "-apple-system, BlinkMacSystemFont, sans-serif",
        display: "flex", flexDirection: "column", gap: "20px",
        border: "1px solid #F0F0F0"
    },
    header: { display: "flex", flexDirection: "row", alignItems: "center", gap: "16px" },
    emojiBadge: { minWidth: "64px", minHeight: "64px", borderRadius: "20px", display: "flex", alignItems: "center", justifyContent: "center", transition: "all 0.3s" },
    emoji: { fontSize: "32px" },
    textContainer: { display: "flex", flexDirection: "column", gap: "2px" },
    title: { margin: 0, fontSize: "18px", fontWeight: "800", color: "#000" },
    desc: { margin: 0, fontSize: "13px", color: "#888", fontWeight: "500", lineHeight: "1.4" },
    button: { border: "none", padding: "16px", borderRadius: "18px", cursor: "pointer", fontWeight: "700", fontSize: "14px", transition: "all 0.3s ease", marginTop: "4px" },
    footerLinks: { display: "flex", justifyContent: "center", gap: "8px", fontSize: "11px", color: "#999", marginTop: "4px" },
    link: { color: "#999", textDecoration: "none", cursor: "pointer" }
}

addPropertyControls(CookieManager, {
    googleAnalyticsID: { type: ControlType.String, title: "GA4 ID", placeholder: "G-XXXXXXXX" },
    metaPixelID: { type: ControlType.String, title: "Meta Pixel", placeholder: "12345678" },
    accentColor: { type: ControlType.Color, title: "Brand Color", defaultValue: "#E84511" },
    privacyLink: { type: ControlType.String, title: "Privacy Link", defaultValue: "/de/datenschutz" },
    imprintLink: { type: ControlType.String, title: "Imprint Link", defaultValue: "/de/impressum" },
    startClosed: { type: ControlType.Boolean, title: "Debug: Show?", defaultValue: false, enabledTitle: "Force Show", disabledTitle: "Auto" },
})

Schritt 2: Der Widerruf (Footer Reset)

Damit es DSGVO-konform ist (Widerrufsrecht), muss der Nutzer seine Entscheidung jederzeit ändern können.

Erstelle eine neue Code Datei in Overrides (z.B. CookieReset.tsx) und füge diesen Code ein:

import { ComponentType } from "react"

export function CookieReset(Component): ComponentType {
    return (props: any) => {
        return (
            <Component
                {...props}
                style={{ ...props.style, cursor: "pointer" }}
                onClick={(e: any) => {
                    e.preventDefault()
                    // Löscht die Entscheidung
                    localStorage.removeItem("consent_granted")
                    // Lädt die Seite neu -> Banner erscheint
                    window.location.reload()
                }}
            />
        )
    }
}

Verbindung in Framer:

  1. Textzeile einfügen: Schreibe irgendwo in deinen Footer einen Text wie "Cookie-Einstellungen".
  2. Text auswählen: Klicke diesen Text auf dem Canvas an.
  3. Code Overrides finden: Schau ganz unten rechts in der Seitenleiste nach der Sektion "Code Overrides".
  4. Override hinzufügen: Klicke auf das kleine + Zeichen.
  5. Einstellen: Wähle bei File: CookieReset und bei Override: CookieReset.

Schritt 3: Setup in Framer

1. Komponente platzieren

Ziehe die CookieManager Komponente auf deinen Canvas.

Wichtig: Platziere sie am besten in deiner Navigation-Komponente oder deinem Footer. Warum? Diese Komponenten sind meistens auf allen Seiten vorhanden. Wenn du es nur auf der Home-Seite einfügst, wird das Cookie-Banner auf Unterseiten nicht geladen (und Analytics auch nicht, selbst wenn Consent da wäre).

2. IDs eintragen

Klicke die Komponente an und trage rechts in den Properties deine G-XXXXXX und Meta Pixel ID ein.

3. Site Settings säubern (Kritisch!)

Gehe in die Framer Site Settings → General → Custom Code. Lösche alles raus, was mit Google Analytics oder Meta zu tun hat. Wenn der Code dort drin steht, lädt er sofort beim Seitenstart, egal was dein React-Code macht. Das ist der häufigste Fehler.

Schritt 4: Publish & Test

  1. Öffne die Seite im Incognito Tab.
  2. Banner sollte erscheinen.
  3. Öffne DevTools (F12) → Network.
  4. Suche nach "google" oder "collect". Es sollte nichts zu sehen sein.
  5. Klicke "Auswahl bestätigen" (im Full Support Modus).
  6. Jetzt sollten die Requests im Network-Tab auftauchen.

Erweiterte Dev-Tipps

  • TypeScript Errors: Framer's Editor meckert manchmal bei window.dataLayer oder window.fbq. Der Code oben enthält bereits die declare global Definition, um dies zu fixen.
  • GTM (Tag Manager): Wenn du den Google Tag Manager (GTM) nutzt statt direkt GA4, änderst du die URL im script.src einfach auf die GTM-Url. Die Logik bleibt identisch.
  • Re-Render Safety: Der useEffect läuft beim Navigieren in Framer nicht ständig neu, wenn die Komponente im Wrapper (Nav/Footer) liegt, der persistent bleibt. Der Check auf vorhandene Script-IDs verhindert doppeltes Laden.

Keine Zeit für den Einbau?

Oder hast du ein komplexeres Problem in Framer, Shopify oder bei deinem Tracking-Setup? Ich begleite Agenturen und Marken als Tech-Partner bei der Umsetzung anspruchsvoller Web-Projekte.

15-Min Tech-Check buchen →

Komplett kostenlos & unverbindlich.