Retour au catalogue
Cookie Consent
Banniere RGPD de consentement aux cookies avec options granulaires et animation slide-up.
bannersmedium Both Responsive a11y
minimalcorporateuniversalstacked
Theme
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Cookie, Shield, ChevronDown, ChevronUp, X } from "lucide-react";
interface CookieCategory {
id: string;
label: string;
description: string;
required: boolean;
}
interface BannerCookieConsentProps {
title?: string;
description?: string;
acceptLabel?: string;
rejectLabel?: string;
customizeLabel?: string;
privacyLink?: string;
privacyLabel?: string;
categories?: CookieCategory[];
}
const EASE: [number, number, number, number] = [0.16, 1, 0.3, 1];
export default function BannerCookieConsent({
title = "Nous respectons votre vie privee",
description = "Ce site utilise des cookies pour ameliorer votre experience.",
acceptLabel = "Tout accepter",
rejectLabel = "Tout refuser",
customizeLabel = "Personnaliser",
privacyLink = "#",
privacyLabel = "Politique de confidentialite",
categories = [],
}: BannerCookieConsentProps) {
const [visible, setVisible] = useState(true);
const [expanded, setExpanded] = useState(false);
const [toggles, setToggles] = useState<Record<string, boolean>>(() => {
const initial: Record<string, boolean> = {};
categories.forEach((c) => {
initial[c.id] = c.required;
});
return initial;
});
const handleToggle = (id: string, required: boolean) => {
if (required) return;
setToggles((prev) => ({ ...prev, [id]: !prev[id] }));
};
if (!visible) return null;
return (
<AnimatePresence>
{visible && (
<motion.div
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }}
transition={{ duration: 0.5, ease: EASE }}
style={{
position: "fixed",
bottom: 24,
left: "50%",
transform: "translateX(-50%)",
zIndex: 9999,
width: "calc(100% - 2rem)",
maxWidth: 540,
}}
>
<div
style={{
background: "var(--color-background-card)",
border: "1px solid var(--color-border)",
borderRadius: "var(--radius-xl)",
padding: "1.5rem",
boxShadow: "0 25px 50px -12px rgba(0,0,0,0.25)",
backdropFilter: "blur(16px)",
}}
>
{/* Header */}
<div style={{ display: "flex", alignItems: "flex-start", gap: 12, marginBottom: "1rem" }}>
<div
style={{
width: 40,
height: 40,
borderRadius: "var(--radius-lg)",
background: "var(--color-accent-subtle)",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}
>
<Cookie style={{ width: 20, height: 20, color: "var(--color-accent)" }} />
</div>
<div style={{ flex: 1 }}>
<h3
style={{
fontSize: "0.9375rem",
fontWeight: 600,
color: "var(--color-foreground)",
marginBottom: 4,
}}
>
{title}
</h3>
<p style={{ fontSize: "0.8125rem", lineHeight: 1.5, color: "var(--color-foreground-muted)" }}>
{description}
</p>
</div>
<button
onClick={() => setVisible(false)}
style={{
background: "none",
border: "none",
cursor: "pointer",
color: "var(--color-foreground-light)",
padding: 4,
flexShrink: 0,
}}
aria-label="Fermer"
>
<X style={{ width: 16, height: 16 }} />
</button>
</div>
{/* Expandable categories */}
<AnimatePresence>
{expanded && categories.length > 0 && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: EASE }}
style={{ overflow: "hidden", marginBottom: "1rem" }}
>
<div
style={{
borderRadius: "var(--radius-md)",
border: "1px solid var(--color-border)",
overflow: "hidden",
}}
>
{categories.map((cat, i) => (
<div
key={cat.id}
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "0.75rem 1rem",
borderBottom: i < categories.length - 1 ? "1px solid var(--color-border)" : "none",
background: "var(--color-background-alt)",
}}
>
<div>
<p style={{ fontSize: "0.8125rem", fontWeight: 500, color: "var(--color-foreground)" }}>
<Shield
style={{
width: 12,
height: 12,
display: "inline",
marginRight: 6,
color: "var(--color-accent)",
verticalAlign: "middle",
}}
/>
{cat.label}
{cat.required && (
<span style={{ fontSize: "0.6875rem", color: "var(--color-foreground-light)", marginLeft: 6 }}>
(requis)
</span>
)}
</p>
<p style={{ fontSize: "0.75rem", color: "var(--color-foreground-muted)", marginTop: 2 }}>
{cat.description}
</p>
</div>
<button
onClick={() => handleToggle(cat.id, cat.required)}
style={{
width: 40,
height: 22,
borderRadius: 11,
border: "none",
cursor: cat.required ? "not-allowed" : "pointer",
background: toggles[cat.id] ? "var(--color-accent)" : "var(--color-border)",
position: "relative",
transition: "background 0.2s",
flexShrink: 0,
opacity: cat.required ? 0.6 : 1,
}}
>
<motion.div
animate={{ x: toggles[cat.id] ? 20 : 2 }}
transition={{ duration: 0.2, ease: EASE }}
style={{
width: 18,
height: 18,
borderRadius: 9,
background: "var(--color-background)",
position: "absolute",
top: 2,
left: 0,
boxShadow: "0 1px 3px rgba(0,0,0,0.15)",
}}
/>
</button>
</div>
))}
</div>
</motion.div>
)}
</AnimatePresence>
{/* Actions */}
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
<button
onClick={() => setVisible(false)}
style={{
flex: 1,
padding: "0.75rem 1rem",
borderRadius: "var(--radius-md)",
background: "var(--color-accent)",
color: "var(--color-background)",
fontWeight: 600,
fontSize: "0.8125rem",
border: "none",
cursor: "pointer",
minWidth: 100,
}}
>
{acceptLabel}
</button>
<button
onClick={() => setVisible(false)}
style={{
flex: 1,
padding: "0.75rem 1rem",
borderRadius: "var(--radius-md)",
background: "var(--color-background-alt)",
color: "var(--color-foreground)",
fontWeight: 500,
fontSize: "0.8125rem",
border: "1px solid var(--color-border)",
cursor: "pointer",
minWidth: 100,
}}
>
{rejectLabel}
</button>
<button
onClick={() => setExpanded(!expanded)}
style={{
flex: 1,
padding: "0.75rem 1rem",
borderRadius: "var(--radius-md)",
background: "transparent",
color: "var(--color-foreground-muted)",
fontWeight: 500,
fontSize: "0.8125rem",
border: "1px solid var(--color-border)",
cursor: "pointer",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
minWidth: 100,
}}
>
{customizeLabel}
{expanded ? <ChevronUp style={{ width: 14, height: 14 }} /> : <ChevronDown style={{ width: 14, height: 14 }} />}
</button>
</div>
{/* Privacy link */}
<div style={{ textAlign: "center", marginTop: "0.75rem" }}>
<a
href={privacyLink}
style={{
fontSize: "0.75rem",
color: "var(--color-foreground-light)",
textDecoration: "underline",
textUnderlineOffset: 2,
}}
>
{privacyLabel}
</a>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
);
}