Moving Border Button
A button with an animated border that moves along an SVG path using framer-motion
buttonanimatedborderframer-motionsvg
Install dependencies
$npm install framer-motionPreview
Source Code
"use client";
import React from "react";
import {
motion,
useAnimationFrame,
useMotionTemplate,
useMotionValue,
useTransform,
} from "framer-motion";
import { useRef } from "react";
import { cn } from "@/lib/utils";
type MovingBorderButtonProps<T extends React.ElementType> = {
borderRadius?: string;
children: React.ReactNode;
as?: T;
containerClassName?: string;
borderClassName?: string;
duration?: number;
className?: string;
} & Omit<
React.ComponentPropsWithoutRef<T>,
"as" | "children" | "className" | "style"
>;
export default function MovingBorderButton<T extends React.ElementType = "button">(
props: MovingBorderButtonProps<T>
) {
const {
borderRadius = "1.75rem",
children,
as,
containerClassName,
borderClassName,
duration,
className,
...otherProps
} = props;
const Component = (as ?? "button") as React.ElementType;
return (
<Component
className={cn(
"bg-transparent relative text-xl h-16 w-40 p-px overflow-hidden",
containerClassName
)}
style={{
borderRadius: borderRadius,
}}
{...otherProps}
>
<div
className="absolute inset-0"
style={{ borderRadius: `calc(${borderRadius} * 0.96)` }}
>
<MovingBorder duration={duration} rx="30%" ry="30%">
<div
className={cn(
"h-20 w-20 opacity-[0.8] bg-[radial-gradient(#0ea5e9_40%,transparent_60%)]",
borderClassName
)}
/>
</MovingBorder>
</div>
<div
className={cn(
"relative bg-slate-900/80 border border-slate-800 backdrop-blur-xl text-white flex items-center justify-center w-full h-full text-sm antialiased",
className
)}
style={{
borderRadius: `calc(${borderRadius} * 0.96)`,
}}
>
{children}
</div>
</Component>
);
}
type MovingBorderProps = React.SVGProps<SVGSVGElement> & {
children: React.ReactNode;
duration?: number;
rx?: string;
ry?: string;
};
export const MovingBorder = ({
children,
duration = 2000,
rx,
ry,
...otherProps
}: MovingBorderProps) => {
const pathRef = useRef<SVGRectElement | null>(null);
const progress = useMotionValue<number>(0);
useAnimationFrame((time) => {
const length = pathRef.current?.getTotalLength();
if (length) {
const pxPerMillisecond = length / duration;
progress.set((time * pxPerMillisecond) % length);
}
});
const x = useTransform(
progress,
(val) => pathRef.current?.getPointAtLength(val).x
);
const y = useTransform(
progress,
(val) => pathRef.current?.getPointAtLength(val).y
);
const transform = useMotionTemplate`translateX(${x}px) translateY(${y}px) translateX(-50%) translateY(-50%)`;
return (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="none"
className="absolute h-full w-full"
width="100%"
height="100%"
{...otherProps}
>
<rect
fill="none"
width="100%"
height="100%"
rx={rx}
ry={ry}
ref={pathRef}
/>
</svg>
<motion.div
style={{
position: "absolute",
top: 0,
left: 0,
display: "inline-block",
transform,
}}
>
{children}
</motion.div>
</>
);
};