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.
// 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
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.
Yes. Delete the `svg` element above the link. The pill, the pulsing dot and the hover state do not depend on it.
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.
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.
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.
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.