Animated Beam Connecting Integrations

A dark feature section that shows data moving between tools. Three source icons sit on the left, three destinations on the right and a hub in the middle. A curved line joins each icon to the hub and a short pulse of light travels along it, one after another. The lines are measured from the real position of the icons, so they stay attached when the layout changes. On phones the copy sits above the diagram and the diagram keeps its three columns at a smaller size.

Code

// Adapted from Magic UI (https://magicui.design/docs/components/animated-beam)
// Copyright (c) Magic UI. MIT License: https://we.inc/components/licenses
import { useEffect, useId, useRef, useState, type RefObject } from "react";
import { motion, useReducedMotion } from "framer-motion";
import { CalendarDays, CreditCard, Database, FileText, Mail, MessageSquare, Workflow } from "lucide-react";

const sources = [
  { label: "Forms", icon: FileText, curve: 24 },
  { label: "Payments", icon: CreditCard, curve: 0 },
  { label: "Bookings", icon: CalendarDays, curve: -24 },
];

const destinations = [
  { label: "Warehouse", icon: Database, curve: 24 },
  { label: "Email", icon: Mail, curve: 0 },
  { label: "Team chat", icon: MessageSquare, curve: -24 },
];

const facts = [
  { value: "40 ms", label: "median hop" },
  { value: "6", label: "tools connected" },
  { value: "0", label: "events dropped" },
];

type BeamProps = {
  containerRef: RefObject<HTMLElement | null>;
  fromRef: RefObject<HTMLElement | null>;
  toRef: RefObject<HTMLElement | null>;
  /** Bends the line: positive bows up, negative bows down. */
  curvature?: number;
  reverse?: boolean;
  duration?: number;
  delay?: number;
  colorFrom?: string;
  colorTo?: string;
};

/** A curved line between two elements with a pulse of light travelling along it. */
function Beam({ containerRef, fromRef, toRef, curvature = 0, reverse = false, duration = 4, delay = 0, colorFrom = "#34d399", colorTo = "#22d3ee" }: BeamProps) {
  const id = useId();
  const reduce = useReducedMotion();
  const [d, setD] = useState("");
  const [size, setSize] = useState({ w: 0, h: 0 });

  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;
    const update = () => {
      const a = fromRef.current;
      const b = toRef.current;
      if (!a || !b) return;
      const box = container.getBoundingClientRect();
      const ra = a.getBoundingClientRect();
      const rb = b.getBoundingClientRect();
      const x1 = ra.left - box.left + ra.width / 2;
      const y1 = ra.top - box.top + ra.height / 2;
      const x2 = rb.left - box.left + rb.width / 2;
      const y2 = rb.top - box.top + rb.height / 2;
      setSize({ w: box.width, h: box.height });
      setD(`M ${x1},${y1} Q ${(x1 + x2) / 2},${y1 - curvature} ${x2},${y2}`);
    };
    const ro = new ResizeObserver(update);
    ro.observe(container);
    update();
    return () => ro.disconnect();
  }, [containerRef, fromRef, toRef, curvature]);

  const travel = reverse
    ? { x1: ["80%", "-20%"], x2: ["100%", "0%"], y1: ["0%", "0%"], y2: ["0%", "0%"] }
    : { x1: ["20%", "120%"], x2: ["0%", "100%"], y1: ["0%", "0%"], y2: ["0%", "0%"] };

  return (
    <svg aria-hidden="true" fill="none" width={size.w} height={size.h} viewBox={`0 0 ${size.w} ${size.h}`} className="pointer-events-none absolute left-0 top-0">
      <path d={d} stroke="#3b4a63" strokeWidth={2} strokeLinecap="round" />
      <path d={d} stroke={`url(#${id})`} strokeWidth={6} strokeOpacity={0.25} strokeLinecap="round" />
      <path d={d} stroke={`url(#${id})`} strokeWidth={2} strokeLinecap="round" />
      <defs>
        {reduce ? (
          <linearGradient id={id} gradientUnits="userSpaceOnUse" x1="0%" x2="100%" y1="0%" y2="0%">
            <stop stopColor={colorFrom} stopOpacity="0.7" />
            <stop offset="100%" stopColor={colorTo} stopOpacity="0.7" />
          </linearGradient>
        ) : (
          <motion.linearGradient
            id={id}
            gradientUnits="userSpaceOnUse"
            initial={{ x1: "0%", x2: "0%", y1: "0%", y2: "0%" }}
            animate={travel}
            transition={{ delay, duration, ease: [0.45, 0, 0.25, 1], repeat: Infinity, repeatDelay: 0.5 }}
          >
            <stop stopColor={colorFrom} stopOpacity="0" />
            <stop stopColor={colorFrom} />
            <stop offset="32.5%" stopColor={colorTo} />
            <stop offset="100%" stopColor={colorTo} stopOpacity="0" />
          </motion.linearGradient>
        )}
      </defs>
    </svg>
  );
}

export default function AnimatedBeamConnectingIntegrations() {
  const boxRef = useRef<HTMLDivElement>(null);
  const hubRef = useRef<HTMLDivElement>(null);
  const s0 = useRef<HTMLDivElement>(null);
  const s1 = useRef<HTMLDivElement>(null);
  const s2 = useRef<HTMLDivElement>(null);
  const d0 = useRef<HTMLDivElement>(null);
  const d1 = useRef<HTMLDivElement>(null);
  const d2 = useRef<HTMLDivElement>(null);
  const sourceRefs = [s0, s1, s2];
  const destRefs = [d0, d1, d2];

  const node = "relative z-10 flex h-12 w-12 items-center justify-center rounded-2xl border border-slate-700 bg-slate-900 text-slate-200 shadow-lg shadow-black/40 sm:h-14 sm:w-14";

  return (
    <section className="relative overflow-hidden bg-slate-950 px-5 py-16 text-slate-100 sm:px-6 sm:py-24">
      <style>{`
        @keyframes animated-beam-connecting-integrations-halo {
          0%, 100% { opacity: 0.55; transform: scale(1); }
          50% { opacity: 1; transform: scale(1.12); }
        }
        .animated-beam-connecting-integrations-halo { animation: animated-beam-connecting-integrations-halo 3.2s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .animated-beam-connecting-integrations-halo { animation: none; }
        }
      `}</style>
      <span aria-hidden="true" className="pointer-events-none absolute -right-40 top-1/2 h-[520px] w-[520px] -translate-y-1/2 rounded-full bg-emerald-500/10 blur-3xl" />

      <div className="relative mx-auto grid max-w-6xl items-center gap-12 lg:grid-cols-[5fr_6fr] lg:gap-16">
        <div>
          <p className="text-sm font-semibold uppercase tracking-widest text-emerald-300">Relay Sync</p>
          <h2 className="mt-4 text-3xl font-semibold tracking-tight text-white sm:text-5xl">Every tool feeds one customer record.</h2>
          <p className="mt-5 max-w-md text-base leading-7 text-slate-400">A form is filled in, a card is charged, a slot is booked. Relay picks up each event, cleans it and passes it on to the places your team already looks.</p>
          <dl className="mt-8 grid max-w-md grid-cols-3 gap-4 border-t border-slate-800 pt-6">
            {facts.map((f) => (
              <div key={f.label}>
                <dt className="sr-only">{f.label}</dt>
                <dd className="text-2xl font-semibold tracking-tight text-white">{f.value}</dd>
                <dd className="mt-1 text-xs text-slate-400">{f.label}</dd>
              </div>
            ))}
          </dl>
          <a href="#" className="mt-8 inline-flex h-11 items-center rounded-full bg-emerald-400 px-6 text-sm font-semibold text-slate-950 transition hover:bg-emerald-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-300 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950">
            Connect your first tool
          </a>
        </div>

        <div className="rounded-3xl border border-slate-800 bg-slate-900/50 p-5 sm:p-8">
          <div className="flex items-center justify-between text-xs text-slate-400">
            <span className="font-medium uppercase tracking-widest">Sources</span>
            <span className="font-medium uppercase tracking-widest">Destinations</span>
          </div>

          <div ref={boxRef} className="relative mt-5 flex items-stretch justify-between">
            <ul className="flex flex-col justify-between gap-8">
              {sources.map((s, i) => (
                <li key={s.label} className="flex flex-col items-center gap-2">
                  <div ref={sourceRefs[i]} className={node}>
                    <s.icon aria-hidden="true" className="h-5 w-5" />
                  </div>
                  <span className="text-xs text-slate-400">{s.label}</span>
                </li>
              ))}
            </ul>

            <div className="flex flex-col items-center justify-center gap-3">
              <div className="relative">
                <span aria-hidden="true" className="animated-beam-connecting-integrations-halo absolute -inset-4 rounded-full bg-emerald-400/20 blur-xl" />
                <div ref={hubRef} className="relative z-10 flex h-16 w-16 items-center justify-center rounded-3xl bg-gradient-to-br from-emerald-300 to-cyan-400 text-slate-950 shadow-xl shadow-emerald-500/30 sm:h-20 sm:w-20">
                  <Workflow aria-hidden="true" className="h-7 w-7 sm:h-8 sm:w-8" />
                </div>
              </div>
              <span className="relative z-10 rounded-full bg-slate-950 px-2 text-xs font-semibold text-white">Relay</span>
            </div>

            <ul className="flex flex-col justify-between gap-8">
              {destinations.map((s, i) => (
                <li key={s.label} className="flex flex-col items-center gap-2">
                  <div ref={destRefs[i]} className={node}>
                    <s.icon aria-hidden="true" className="h-5 w-5" />
                  </div>
                  <span className="text-xs text-slate-400">{s.label}</span>
                </li>
              ))}
            </ul>

            {sources.map((s, i) => (
              <Beam key={s.label} containerRef={boxRef} fromRef={sourceRefs[i]} toRef={hubRef} curvature={s.curve} delay={i * 0.45} duration={2.6} />
            ))}
            {destinations.map((s, i) => (
              <Beam key={s.label} containerRef={boxRef} fromRef={destRefs[i]} toRef={hubRef} reverse curvature={s.curve} delay={1.3 + i * 0.45} duration={2.6} colorFrom="#a78bfa" colorTo="#22d3ee" />
            ))}
          </div>

          <p className="mt-6 flex items-center gap-2.5 border-t border-slate-800 pt-5 text-sm text-slate-300">
            <span className="h-2 w-2 rounded-full bg-emerald-400" />
            1,284 events passed through in the last hour
          </p>
        </div>
      </div>
    </section>
  );
}

Install with one command: npx shadcn@latest add https://we.inc/r/animated-beam-connecting-integrations.json

When to use it

How to customize it

Accessibility

Questions

How do the lines find the icons?

Each `Beam` takes three refs: the container, the element it starts from and the element it ends at. It reads their positions with `getBoundingClientRect`, works out the two centre points relative to the container and writes an svg path between them.

Do the beams stay attached when the page resizes?

Yes. A `ResizeObserver` on the container recalculates every path whenever its size changes, so the lines follow the icons from phone to desktop widths.

How is the travelling light made?

The line is stroked with a linear gradient that is transparent at both ends and coloured in the middle. The gradient's start and end points are animated from one side of the drawing to the other, which slides the coloured part along the path.

Can I connect elements top to bottom instead of left to right?

The path works in any direction, but the gradient moves along the x axis. For a vertical layout animate `y1` and `y2` in the `travel` object instead of `x1` and `x2`.

Why do the icons sit above the lines?

The icon tiles have `relative z-10` and a solid background, so the lines run from centre to centre but appear to stop at the tile edge. Keep a solid background on anything a beam connects to.

Adapted from Animated Beam in Magic UI, used under its open-source licence. Licences and credits

Related: Hero with Orbiting App Icons Around a Logo, Card with Travelling Border Beam, Three Column Feature Grid with Icons.

More feature sections or all free UI components. Start for free to have a full site built around it.

More in Feature Sections

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap