Retour au catalogue

Marquee Gradient Fade

Deux rows de texte défilant en sens opposés avec fade gradient large sur les bords. Texte alterné normal/serif-italic. Fond avec glow radial central. Ralentissement progressif au hover.

marqueemedium Both Responsive a11y
elegantminimaleditorialsaasagencyuniversalfullscreen
Theme
"use client";

import { useState } from "react";
import { motion, useAnimation } from "framer-motion";

interface MarqueeGradientFadeProps {
  rowOne?: string[];
  rowTwo?: string[];
  speedRow1?: number;
  speedRow2?: number;
  label?: string;
}

interface MarqueeItem {
  text: string;
  serif: boolean;
}

function buildItems(items: string[]): MarqueeItem[] {
  return items.map((text, i) => ({ text, serif: i % 3 === 1 }));
}

function MarqueeTrack({
  items,
  duration,
  reverse,
  slowDuration,
  isSlowing,
}: {
  items: MarqueeItem[];
  duration: number;
  reverse: boolean;
  slowDuration: number;
  isSlowing: boolean;
}) {
  const controls = useAnimation();
  const doubled = [...items, ...items];

  const from = reverse ? "-50%" : "0%";
  const to = reverse ? "0%" : "-50%";

  const activeDuration = isSlowing ? slowDuration : duration;

  return (
    <div style={{ overflow: "hidden", position: "relative" }}>
      <motion.div
        animate={controls}
        key={`track-${isSlowing}-${activeDuration}`}
        initial={{ x: from }}
        style={{
          display: "flex",
          whiteSpace: "nowrap",
          willChange: "transform",
        }}
        onViewportEnter={() => {
          controls.start({
            x: [from, to],
            transition: {
              duration: activeDuration,
              ease: "linear",
              repeat: Infinity,
              repeatType: "loop",
            },
          });
        }}
      >
        {doubled.map((item, i) => (
          <span
            key={`${item.text}-${i}`}
            style={{
              display: "inline-flex",
              alignItems: "center",
              flexShrink: 0,
              paddingRight: "3rem",
              gap: "3rem",
              fontSize: "clamp(1.125rem, 2.25vw, 1.625rem)",
              letterSpacing: "-0.015em",
              lineHeight: 1,
              color: item.serif
                ? "var(--color-foreground-muted)"
                : "var(--color-foreground)",
              fontWeight: item.serif ? 400 : 600,
              fontStyle: item.serif ? "italic" : "normal",
              fontFamily: item.serif ? "var(--font-serif)" : "inherit",
            }}
          >
            {item.text}
            <span
              aria-hidden
              style={{
                width: 5,
                height: 5,
                borderRadius: "var(--radius-full)",
                background: "var(--color-accent)",
                opacity: 0.6,
                flexShrink: 0,
                display: "inline-block",
              }}
            />
          </span>
        ))}
      </motion.div>
    </div>
  );
}

export default function MarqueeGradientFade({
  rowOne = [],
  rowTwo = [],
  speedRow1 = 28,
  speedRow2 = 22,
  label,
}: MarqueeGradientFadeProps) {
  const [isSlowing, setIsSlowing] = useState(false);
  const itemsOne = buildItems(rowOne);
  const itemsTwo = buildItems(rowTwo);

  const slowMultiplier = 3.5;

  return (
    <section
      aria-label={label ?? "Marquee"}
      onMouseEnter={() => setIsSlowing(true)}
      onMouseLeave={() => setIsSlowing(false)}
      style={{
        position: "relative",
        overflow: "hidden",
        padding: "4rem 0",
        background: "var(--color-background)",
        cursor: "default",
      }}
    >
      {/* Radial glow — centre */}
      <div
        aria-hidden
        style={{
          position: "absolute",
          inset: 0,
          background:
            "radial-gradient(ellipse 60% 70% at 50% 50%, color-mix(in srgb, var(--color-accent) 8%, transparent), transparent 70%)",
          pointerEvents: "none",
          zIndex: 0,
        }}
      />

      {/* Left fade */}
      <div
        aria-hidden
        style={{
          position: "absolute",
          left: 0,
          top: 0,
          bottom: 0,
          width: "22%",
          background:
            "linear-gradient(to right, var(--color-background) 10%, transparent 100%)",
          zIndex: 2,
          pointerEvents: "none",
        }}
      />

      {/* Right fade */}
      <div
        aria-hidden
        style={{
          position: "absolute",
          right: 0,
          top: 0,
          bottom: 0,
          width: "22%",
          background:
            "linear-gradient(to left, var(--color-background) 10%, transparent 100%)",
          zIndex: 2,
          pointerEvents: "none",
        }}
      />

      {/* Rows */}
      <div style={{ position: "relative", zIndex: 1, display: "flex", flexDirection: "column", gap: "1.25rem" }}>
        {itemsOne.length > 0 && (
          <MarqueeTrack
            items={itemsOne}
            duration={speedRow1}
            reverse={false}
            slowDuration={speedRow1 * slowMultiplier}
            isSlowing={isSlowing}
          />
        )}
        {itemsTwo.length > 0 && (
          <MarqueeTrack
            items={itemsTwo}
            duration={speedRow2}
            reverse={true}
            slowDuration={speedRow2 * slowMultiplier}
            isSlowing={isSlowing}
          />
        )}
      </div>
    </section>
  );
}

Avis

Marquee Gradient Fade — React Marquee Section — Incubator