Moving Border

A border that moves around the container. Perfect for making your buttons stand out.

Installation

Install Dependencies

npm i svelte-motion clsx tailwind-merge

Add util file

src/lib/utils/cn.ts
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
        
export function cn(...inputs: ClassValue[]) {
    return twMerge(clsx(inputs));
}

Copy the source code

src/lib/components/ui/MovingBorder/MovingBorder.svelte
<script lang="ts">
	import { onMount, onDestroy } from 'svelte';
	import { writable } from 'svelte/store';

	export let duration = 2000;
	export let rx = '0';
	export let ry = '0';

	let pathElement: SVGRectElement;
	let progress = writable(0);

	let animationFrameId: number;
	onMount(() => {
		const animate = (time: number) => {
			const length = pathElement.getTotalLength();
			const pxPerMillisecond = length / duration;
			const newProgress = (time * pxPerMillisecond) % length;
			progress.set(newProgress === 0 ? 0 : newProgress); // Reset progress to 0 instantly
			animationFrameId = requestAnimationFrame(animate);
		};
		animationFrameId = requestAnimationFrame(animate);
	});

	onDestroy(() => {
		if (typeof cancelAnimationFrame === 'function') {
			cancelAnimationFrame(animationFrameId);
		}
	});

	let x = 0;
	let y = 0;
	$: if (pathElement) {
		const point = pathElement.getPointAtLength($progress);
		x = point.x;
		y = point.y;
	}

	$: transform = `translateX(${x}px) translateY(${y}px) translateX(-50%) translateY(-50%)`;
</script>

<svg
	xmlns="http://www.w3.org/2000/svg"
	preserveAspectRatio="none"
	class="absolute h-full w-full"
	width="100%"
	height="100%"
>
	<rect bind:this={pathElement} fill="none" width="100%" height="100%" {rx} {ry} />
</svg>

<div style="position: absolute; top: 0; left: 0; display: inline-block;" style:transform>
	<slot />
</div>
src/lib/components/ui/MovingBorder/Button.ts
<script lang="ts">
	import { cn } from '@/utils';
	import MovingBorder from './MovingBorder.svelte';

	export let borderRadius: string | undefined = '1.75rem';
	export let containerClassName: string | undefined = undefined;
	export let borderClassName: string | undefined = undefined;
	export let duration: number | undefined = 2000;
	export let className: string | undefined = undefined;
</script>

<button
	class={cn(
		'relative h-16 w-40  overflow-hidden bg-transparent p-[1px] text-xl ',
		containerClassName
	)}
	style={`border-radius: ${borderRadius};`}
	{...$$props}
>
	<div class="absolute inset-0" style={`border-radius: calc(${borderRadius} * 0.96);`}>
		<MovingBorder {duration} rx="30%" ry="30%">
			<div
				class={cn(
					'h-20 w-20 bg-[radial-gradient(var(--sky-500)_40%,transparent_60%)] opacity-[0.8]',
					borderClassName
				)}
			/>
		</MovingBorder>
	</div>

	<div
		class={cn(
			'relative flex h-full w-full items-center justify-center border border-slate-800 bg-slate-900/[0.8] text-sm text-white antialiased backdrop-blur-xl',
			className
		)}
		style={`border-radius: calc(${borderRadius} * 0.96);`}
	>
		<slot />
	</div>
</button>
src/lib/components/ui/MovingBorder/index.ts
import MovingBorder from './MovingBorder.svelte';
import Button from './Button.svelte';

export { MovingBorder, Button };

Props

MovingBorder
Prop Type Description
borderRadius string | undefined Default: "1.75rem". Defines the border radius of the button.
containerClassName string | undefined Additional CSS classes to be applied to the button container.
borderClassName string | undefined Additional CSS classes to be applied to the button border.
duration number Default: 2000. The duration of the border animation in milliseconds.
className string | undefined Additional CSS classes to be applied to the button.