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>
  );
}

Avis

Ribbon Announcement — React Ribbon Section — Incubator