Announcement Badge with Light Tracing a Circuit Line

A pill-shaped announcement badge for the top of a hero. It has a pulsing status dot, a small label, one line of news and a chevron. Above it sit two thin stepped wires, like traces on a circuit board, and a soft light travels along each one and arrives at the badge. The demo places it in a dark hosting hero with a headline, a paragraph, two buttons and a row of plain colour variants. On phones the wire drawing shrinks with the screen and the badge text stays on one line.

Code

// Adapted from Eldora UI (https://github.com/karthikmudunuri/eldoraui/blob/main/apps/www/registry/eldoraui/animated-badge.tsx)
// Copyright (c) Mudunuri Bhaskara Karthikeya Varma. MIT License: https://we.inc/components/licenses
import type { CSSProperties } from "react";
import { motion, useReducedMotion } from "framer-motion";
import { ArrowRight, ChevronRight } from "lucide-react";

const announcement = { label: "New", text: "Edge caching is live in 14 regions", color: "#34d399" };

const variants = [
  { text: "Changelog 4.2", color: "#38bdf8" },
  { text: "Status: all systems normal", color: "#34d399" },
  { text: "Maintenance on Sunday", color: "#fbbf24" },
];

// Both wires end at the top centre of the badge (x 200, y 120 in a 400 x 120 drawing).
const wires = [
  { id: "left", d: "M 0 12 h 110 q 10 0 10 10 v 28 q 0 10 10 10 h 60 q 10 0 10 10 v 50", delay: "0s" },
  { id: "right", d: "M 400 30 h -90 q -10 0 -10 10 v 34 q 0 10 -10 10 h -80 q -10 0 -10 10 v 26", delay: "1.6s" },
];

function PulseDot({ color }: { color: string }) {
  return (
    <span className="relative flex h-2 w-2 shrink-0" aria-hidden="true">
      <span className="absolute inline-flex h-full w-full animate-ping rounded-full opacity-70 motion-reduce:animate-none" style={{ backgroundColor: color }} />
      <span className="relative inline-flex h-2 w-2 rounded-full" style={{ backgroundColor: color }} />
    </span>
  );
}

export default function AnnouncementBadgeWithLightTracingACircuitLine() {
  const reduce = useReducedMotion();
  const rise = (delay: number) => ({
    initial: reduce ? false : { opacity: 0, y: 18, filter: "blur(6px)" },
    animate: { opacity: 1, y: 0, filter: "blur(0px)" },
    transition: { duration: 0.6, delay, ease: [0.22, 1, 0.36, 1] as const },
  });

  return (
    <section className="relative overflow-hidden bg-[#06110e] px-4 pb-20 pt-10 text-white sm:px-6 sm:pb-28">
      <style>{`
        @keyframes announcement-badge-with-light-tracing-a-circuit-line-run {
          0% { offset-distance: 0%; opacity: 0; }
          8% { opacity: 1; }
          55% { offset-distance: 100%; opacity: 1; }
          62%, 100% { offset-distance: 100%; opacity: 0; }
        }
        .announcement-badge-with-light-tracing-a-circuit-line-light {
          offset-anchor: 0 0;
          offset-rotate: 0deg;
          animation: announcement-badge-with-light-tracing-a-circuit-line-run 3.2s linear infinite;
        }
        @media (prefers-reduced-motion: reduce) {
          .announcement-badge-with-light-tracing-a-circuit-line-light { animation: none; offset-distance: 92%; }
        }
      `}</style>

      <div className="pointer-events-none absolute inset-x-0 top-0 h-[420px] bg-[radial-gradient(60%_60%_at_50%_0%,rgba(16,185,129,0.18),transparent)]" aria-hidden="true" />

      <div className="relative mx-auto flex max-w-3xl flex-col items-center text-center">
        {/* The wires: a faint track, plus a soft light that is only visible where it crosses the track. */}
        <svg viewBox="0 0 400 120" fill="none" className="h-[96px] w-[320px] sm:h-[120px] sm:w-[400px]" aria-hidden="true">
          <defs>
            <radialGradient id="announcement-badge-light">
              <stop offset="0%" stopColor="#ffffff" />
              <stop offset="25%" stopColor={announcement.color} />
              <stop offset="100%" stopColor={announcement.color} stopOpacity="0" />
            </radialGradient>
            {wires.map((w) => (
              <mask key={w.id} id={`announcement-badge-mask-${w.id}`} maskUnits="userSpaceOnUse" x="0" y="0" width="400" height="120">
                <path d={w.d} stroke="white" strokeWidth="2.5" />
              </mask>
            ))}
          </defs>
          {wires.map((w) => (
            <g key={w.id}>
              <path d={w.d} stroke="rgba(255,255,255,0.14)" strokeWidth="1" />
              <g mask={`url(#announcement-badge-mask-${w.id})`}>
                <circle
                  r="46"
                  fill="url(#announcement-badge-light)"
                  className="announcement-badge-with-light-tracing-a-circuit-line-light"
                  style={{ offsetPath: `path("${w.d}")`, animationDelay: w.delay } as CSSProperties}
                />
              </g>
            </g>
          ))}
          <circle cx="0" cy="12" r="2.5" fill="rgba(255,255,255,0.25)" />
          <circle cx="400" cy="30" r="2.5" fill="rgba(255,255,255,0.25)" />
        </svg>

        <motion.a
          {...rise(0)}
          href="#"
          className="group relative inline-flex min-h-[40px] items-center gap-3 rounded-full border border-white/15 bg-black/60 py-1.5 pl-4 pr-3 text-sm text-emerald-50 shadow-[0_0_32px_-6px_rgba(52,211,153,0.55)] transition-colors hover:border-emerald-400/50 focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-400 focus-visible:ring-offset-2 focus-visible:ring-offset-[#06110e]"
        >
          <PulseDot color={announcement.color} />
          <span className="text-xs font-semibold uppercase tracking-widest text-emerald-300">{announcement.label}</span>
          <span className="h-4 w-px bg-white/20" aria-hidden="true" />
          <span className="font-medium">{announcement.text}</span>
          <ChevronRight className="h-4 w-4 text-emerald-200/70 transition-transform duration-200 group-hover:translate-x-0.5" aria-hidden="true" />
        </motion.a>

        <motion.h1 {...rise(0.1)} className="mt-9 text-4xl font-semibold tracking-tight sm:text-6xl">
          Pages that load before
          <br className="hidden sm:block" /> the thumb leaves the screen
        </motion.h1>
        <motion.p {...rise(0.18)} className="mt-6 max-w-xl text-lg leading-relaxed text-emerald-100/70">
          Halden Deploy keeps a warm copy of your site next to every visitor. Push to your main branch and the new version is everywhere in under a minute.
        </motion.p>
        <motion.div {...rise(0.26)} className="mt-9 flex flex-wrap justify-center gap-3">
          <a href="#" className="inline-flex h-12 items-center gap-2 rounded-full bg-emerald-400 px-6 text-sm font-semibold text-emerald-950 transition-colors hover:bg-emerald-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-[#06110e]">
            Start deploying <ArrowRight className="h-4 w-4" aria-hidden="true" />
          </a>
          <a href="#" className="inline-flex h-12 items-center rounded-full border border-white/20 px-6 text-sm font-semibold text-white transition-colors hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-[#06110e]">
            Read the release notes
          </a>
        </motion.div>

        <motion.ul {...rise(0.34)} className="mt-14 flex flex-wrap justify-center gap-3" aria-label="Badge colour variants">
          {variants.map((v) => (
            <li key={v.text} className="inline-flex min-h-[36px] items-center gap-2.5 rounded-full border border-white/10 bg-white/5 px-3.5 text-xs font-medium text-white/80">
              <PulseDot color={v.color} />
              {v.text}
            </li>
          ))}
        </motion.ul>
      </div>
    </section>
  );
}

Install with one command: npx shadcn@latest add https://we.inc/r/announcement-badge-with-light-tracing-a-circuit-line.json

When to use it

How to customize it

Accessibility

Questions

How does the light follow the line?

Each light is an SVG circle with a CSS `offset-path` that uses the same path data as the wire. A keyframe animation moves `offset-distance` from 0% to 100%. The circle is much wider than the wire, and an SVG mask cut to the wire's shape hides everything except the part lying on the line.

Can I use the badge without the wires?

Yes. Delete the `svg` element above the link. The pill, the pulsing dot and the hover state do not depend on it.

Why does the light pause between runs?

The keyframes finish the trip at 55% of the cycle and fade the light out, then wait. A constant loop reads as a loading spinner, while a pause makes each run feel like a signal arriving.

Will it work on a light background?

Yes. Change the section background, set the wire stroke to a dark colour with low opacity such as `rgba(0,0,0,0.12)`, and give the badge a white fill with a grey border. Pick a darker light colour so it shows on white.

Does `offset-path` work in all browsers?

It works in current Chrome, Edge, Firefox and Safari. In a browser without it the lights simply stay at the start of each wire and the badge is unaffected.

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

Related: Animated Gradient Text with Announcement Pill, Circuit Board Diagram with Pulses Flowing into a Chip, Centered Hero with Two Buttons and Logo Row.

More alerts and feedback or all free UI components. Start for free to have a full site built around it.

More in Feedback

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