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:
- Textzeile einfügen: Schreibe irgendwo in deinen Footer einen Text wie "Cookie-Einstellungen".
- Text auswählen: Klicke diesen Text auf dem Canvas an.
- Code Overrides finden: Schau ganz unten rechts in der Seitenleiste nach der Sektion "Code Overrides".
- Override hinzufügen: Klicke auf das kleine + Zeichen.
- 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.
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
- Öffne die Seite im Incognito Tab.
- Banner sollte erscheinen.
- Öffne DevTools (F12) → Network.
- Suche nach "google" oder "collect". Es sollte nichts zu sehen sein.
- Klicke "Auswahl bestätigen" (im Full Support Modus).
- Jetzt sollten die Requests im Network-Tab auftauchen.
Erweiterte Dev-Tipps
- TypeScript Errors: Framer's Editor meckert manchmal bei
window.dataLayeroderwindow.fbq. Der Code oben enthält bereits diedeclare globalDefinition, um dies zu fixen. - GTM (Tag Manager): Wenn du den Google Tag Manager (GTM) nutzt statt direkt GA4,
änderst du die URL im
script.srceinfach auf die GTM-Url. Die Logik bleibt identisch. - Re-Render Safety: Der
useEffectlä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.
Komplett kostenlos & unverbindlich.