Retour au catalogue
Ribbon Announcement
Bannière d'annonce compacte (48–56px) en haut de page. Badge pulsant, shimmer animé, flèche hover avec bounce, fermeture via AnimatePresence. Mode marquee optionnel pour boucler plusieurs messages.
ribbonmedium Both Responsive a11y
boldminimalcorporatesaasecommerceagencyuniversalcentered
Theme
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { X, ArrowRight, Zap } from "lucide-react";
interface RibbonItem { label: string }
interface RibbonAnnouncementProps {
badge?: string;
message?: string;
linkLabel?: string;
linkUrl?: string;
marquee?: boolean;
items?: RibbonItem[];
}
function MarqueeTrack({ items }: { items: RibbonItem[] }) {
const repeated = [...items, ...items];
return (
<div style={{ overflow: "hidden", flex: 1, minWidth: 0 }}>
<motion.div
animate={{ x: ["0%", "-50%"] }}
transition={{ duration: 22, repeat: Infinity, ease: "linear" }}
style={{ display: "flex", alignItems: "center", gap: "2.5rem", width: "fit-content" }}
>
{repeated.map((item, i) => (
<span key={i} style={{ display: "inline-flex", alignItems: "center", gap: "0.5rem", whiteSpace: "nowrap" }}>
<span style={{ display: "inline-block", width: 3, height: 3, borderRadius: "50%", backgroundColor: "currentColor", opacity: 0.5, flexShrink: 0 }} />
<span style={{ fontSize: "0.8125rem", fontWeight: 500 }}>{item.label}</span>
</span>
))}
</motion.div>
</div>
);
}
export default function RibbonAnnouncement({
badge = "NEW",
message = "Introducing our new AI-powered workflow engine",
linkLabel = "Learn more",
linkUrl = "#",
marquee = false,
items = [],
}: RibbonAnnouncementProps) {
const [dismissed, setDismissed] = useState(false);
return (
<AnimatePresence initial={false}>
{!dismissed && (
<motion.div
key="ribbon"
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.35, ease: [0.4, 0, 0.2, 1] }}
style={{ overflow: "hidden" }}
>
<div
style={{
background: `linear-gradient(90deg, color-mix(in srgb, var(--color-accent) 92%, var(--color-foreground) 8%) 0%, var(--color-accent) 50%, color-mix(in srgb, var(--color-accent) 90%, var(--color-background) 10%) 100%)`,
color: "var(--color-background)",
height: "clamp(48px, 6vw, 56px)",
display: "flex",
alignItems: "center",
position: "relative",
overflow: "hidden",
}}
>
{/* Shimmer */}
<motion.div
aria-hidden
style={{ position: "absolute", inset: 0, background: "linear-gradient(105deg, transparent 40%, rgba(255,255,255,0.10) 50%, transparent 60%)", pointerEvents: "none" }}
animate={{ x: ["-100%", "200%"] }}
transition={{ duration: 3.5, repeat: Infinity, ease: "easeInOut", repeatDelay: 2 }}
/>
<div className="mx-auto flex items-center w-full" style={{ maxWidth: "var(--container-max-width)", paddingLeft: "var(--container-padding-x)", paddingRight: "var(--container-padding-x)", gap: "0.75rem" }}>
{/* Badge */}
{badge && (
<motion.span
animate={{ opacity: [1, 0.55, 1] }}
transition={{ duration: 2.4, repeat: Infinity, ease: "easeInOut" }}
style={{ display: "inline-flex", alignItems: "center", gap: "0.3rem", padding: "0.1rem 0.55rem", borderRadius: "var(--radius-full, 9999px)", fontSize: "0.65rem", fontWeight: 700, letterSpacing: "0.1em", textTransform: "uppercase", background: "rgba(0,0,0,0.18)", flexShrink: 0 }}
>
<Zap style={{ width: 9, height: 9, fill: "currentColor", flexShrink: 0 }} aria-hidden />
{badge}
</motion.span>
)}
{/* Content */}
{marquee && items.length > 0 ? (
<MarqueeTrack items={items} />
) : (
<span style={{ flex: 1, minWidth: 0, fontSize: "0.8125rem", fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
{message}
</span>
)}
{/* Link with bouncing arrow */}
{linkLabel && (
<motion.a
href={linkUrl}
initial="rest"
whileHover="hover"
style={{ display: "inline-flex", alignItems: "center", gap: "0.3rem", fontSize: "0.8125rem", fontWeight: 600, flexShrink: 0, opacity: 0.9, textDecoration: "none", color: "inherit", marginRight: "0.5rem" }}
>
{linkLabel}
<motion.span
variants={{ rest: { x: 0 }, hover: { x: 4 } }}
transition={{ type: "spring", stiffness: 400, damping: 20 }}
style={{ display: "inline-flex" }}
>
<ArrowRight style={{ width: 13, height: 13 }} aria-hidden />
</motion.span>
</motion.a>
)}
{/* Close */}
<motion.button
onClick={() => setDismissed(true)}
aria-label="Fermer l'annonce"
style={{ display: "inline-flex", alignItems: "center", justifyContent: "center", padding: "0.25rem", borderRadius: "var(--radius-full, 9999px)", background: "rgba(0,0,0,0.12)", border: "none", cursor: "pointer", color: "inherit", flexShrink: 0 }}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.92 }}
transition={{ duration: 0.15 }}
>
<X style={{ width: 14, height: 14 }} aria-hidden />
</motion.button>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
);
}