Infinite scrolling cards with electric borders — dramatic showcase in seconds
Scroll or let it auto-scroll • Hover to pause
Copy URL or Code
Paste to your AI coding assistant and customize:
Done. Your AI handles the rest.
Fully customizable. Combines InfiniteScroll with ElectricBorder for dramatic card showcases. Each card can have its own electric color.
// This requires two components: InfiniteScroll and ElectricBorder
// See /infinite-scroll/ai and create ElectricBorder below
// === ElectricBorder.tsx ===
"use client";
import React, { useRef, useEffect, useCallback } from "react";
export interface ElectricBorderProps {
width?: number;
height?: number;
borderRadius?: number;
color?: string;
lineWidth?: number;
speed?: number;
displacement?: number;
className?: string;
children?: React.ReactNode;
}
export default function ElectricBorder({
width = 354,
height = 504,
borderRadius = 24,
color = "#4A9FFF",
lineWidth = 1,
speed = 1.5,
displacement = 60,
className = "",
children,
}: ElectricBorderProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const frameRef = useRef<number | undefined>(undefined);
const timeRef = useRef({ elapsed: 0, lastTick: 0 });
const prng = useCallback((seed: number) => {
return (Math.sin(seed * 12.9873) * 43758.2341) % 1;
}, []);
const coherentNoise = useCallback((px: number, py: number) => {
const ix = Math.floor(px);
const iy = Math.floor(py);
const fx = px - ix;
const fy = py - iy;
const va = prng(ix + iy * 61);
const vb = prng(ix + 1 + iy * 61);
const vc = prng(ix + (iy + 1) * 61);
const vd = prng(ix + 1 + (iy + 1) * 61);
const sx = fx * fx * (3.0 - 2.0 * fx);
const sy = fy * fy * (3.0 - 2.0 * fy);
return va * (1 - sx) * (1 - sy) + vb * sx * (1 - sy) + vc * (1 - sx) * sy + vd * sx * sy;
}, [prng]);
const fbmNoise = useCallback((position: number, layers: number, persistence: number, roughness: number, baseAmp: number, baseFreq: number, time: number = 0, channel: number = 0, dampening: number = 1.0) => {
let result = 0, amp = baseAmp, freq = baseFreq;
for (let i = 0; i < layers; i++) {
let layerAmp = amp;
if (i === 0) layerAmp *= dampening;
result += layerAmp * coherentNoise(freq * position + channel * 100, time * freq * 0.28);
freq *= persistence;
amp *= roughness;
}
return result;
}, [coherentNoise]);
const arcPoint = useCallback((cx: number, cy: number, r: number, startAngle: number, sweep: number, t: number) => {
const angle = startAngle + t * sweep;
return { x: cx + r * Math.cos(angle), y: cy + r * Math.sin(angle) };
}, []);
const perimeterPoint = useCallback((t: number, x0: number, y0: number, w: number, h: number, r: number) => {
const edgeW = w - 2 * r, edgeH = h - 2 * r, cornerLen = (Math.PI * r) / 2;
const totalLen = 2 * edgeW + 2 * edgeH + 4 * cornerLen;
const dist = t * totalLen;
let acc = 0;
if (dist <= acc + edgeW) return { x: x0 + r + ((dist - acc) / edgeW) * edgeW, y: y0 };
acc += edgeW;
if (dist <= acc + cornerLen) return arcPoint(x0 + w - r, y0 + r, r, -Math.PI / 2, Math.PI / 2, (dist - acc) / cornerLen);
acc += cornerLen;
if (dist <= acc + edgeH) return { x: x0 + w, y: y0 + r + ((dist - acc) / edgeH) * edgeH };
acc += edgeH;
if (dist <= acc + cornerLen) return arcPoint(x0 + w - r, y0 + h - r, r, 0, Math.PI / 2, (dist - acc) / cornerLen);
acc += cornerLen;
if (dist <= acc + edgeW) return { x: x0 + w - r - ((dist - acc) / edgeW) * edgeW, y: y0 + h };
acc += edgeW;
if (dist <= acc + cornerLen) return arcPoint(x0 + r, y0 + h - r, r, Math.PI / 2, Math.PI / 2, (dist - acc) / cornerLen);
acc += cornerLen;
if (dist <= acc + edgeH) return { x: x0, y: y0 + h - r - ((dist - acc) / edgeH) * edgeH };
acc += edgeH;
return arcPoint(x0 + r, y0 + r, r, Math.PI, Math.PI / 2, (dist - acc) / cornerLen);
}, [arcPoint]);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const state = timeRef.current;
const cvW = width + displacement * 2;
const cvH = height + displacement * 2;
canvas.width = cvW;
canvas.height = cvH;
const renderFrame = (timestamp: number) => {
const dt = (timestamp - state.lastTick) / 1000;
state.elapsed += dt * speed;
state.lastTick = timestamp;
ctx.clearRect(0, 0, cvW, cvH);
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.lineJoin = "round";
const boxX = displacement, boxY = displacement;
const boxW = cvW - 2 * displacement, boxH = cvH - 2 * displacement;
const r = Math.min(borderRadius, Math.min(boxW, boxH) / 2);
const perim = 2 * (boxW + boxH) + 2 * Math.PI * r;
const samples = Math.floor(perim / 2);
ctx.beginPath();
for (let i = 0; i <= samples; i++) {
const t = i / samples;
const pt = perimeterPoint(t, boxX, boxY, boxW, boxH, r);
const noiseX = fbmNoise(t * 8, 10, 1.58, 0.65, 0.072, 10, state.elapsed, 0, 0);
const noiseY = fbmNoise(t * 8, 10, 1.58, 0.65, 0.072, 10, state.elapsed, 1, 0);
const px = pt.x + noiseX * displacement;
const py = pt.y + noiseY * displacement;
if (i === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
}
ctx.closePath();
ctx.stroke();
frameRef.current = requestAnimationFrame(renderFrame);
};
frameRef.current = requestAnimationFrame(renderFrame);
return () => { if (frameRef.current) cancelAnimationFrame(frameRef.current); };
}, [width, height, borderRadius, color, lineWidth, speed, displacement, fbmNoise, perimeterPoint]);
const glowGradient = `oklch(from ${color} 0.3 calc(c / 2) h / 0.4)`;
return (
<div className={`relative ${className}`} style={{ padding: "2px", borderRadius: `${borderRadius}px`, background: `linear-gradient(-30deg, ${glowGradient}, transparent, ${glowGradient}), linear-gradient(to bottom, oklch(0.185 0 0), oklch(0.185 0 0))` }}>
<div style={{ position: "relative" }}>
<div style={{ position: "relative", width: `${width}px`, height: `${height}px` }}>
<canvas ref={canvasRef} style={{ position: "absolute", top: "50%", left: "50%", transform: "translate(-50%, -50%)", width: `${width + displacement * 2}px`, height: `${height + displacement * 2}px`, pointerEvents: "none" }} />
</div>
<div style={{ border: `2px solid ${color}99`, borderRadius: `${borderRadius}px`, width: "100%", height: "100%", position: "absolute", top: 0, left: 0, filter: "blur(1px)", pointerEvents: "none" }} />
<div style={{ border: `2px solid ${color}`, borderRadius: `${borderRadius}px`, width: "100%", height: "100%", position: "absolute", top: 0, left: 0, filter: "blur(4px)", pointerEvents: "none" }} />
</div>
<div style={{ position: "absolute", width: "100%", height: "100%", top: 0, left: 0, borderRadius: `${borderRadius}px`, filter: "blur(32px)", transform: "scale(1.1)", opacity: 0.3, zIndex: -1, background: `linear-gradient(-30deg, ${color}, transparent, ${color})`, pointerEvents: "none" }} />
{children && <div style={{ position: "absolute", top: 0, left: 0, right: 0, bottom: 0, width: "100%", height: "100%", display: "flex", flexDirection: "column" }}>{children}</div>}
</div>
);
}import InfiniteScroll from "@/components/InfiniteScroll";
import ElectricBorder from "@/components/ElectricBorder";
// Scrolling cards with electric borders
export default function ScrollingCards() {
const cards = [
{ id: 1, color: "#4A9FFF", label: "Card 1" },
{ id: 2, color: "#00ff88", label: "Card 2" },
{ id: 3, color: "#BD93F9", label: "Card 3" },
{ id: 4, color: "#DD8448", label: "Card 4" },
];
return (
<div className="bg-[#0a0a0a] min-h-screen p-8">
<InfiniteScroll
direction="horizontal"
height={220}
gap={24}
autoScroll={true}
autoScrollSpeed={0.5}
pauseOnHover={true}
>
{cards.map((card) => (
<div key={card.id} className="flex-shrink-0">
<ElectricBorder
width={160}
height={200}
color={card.color}
borderRadius={16}
displacement={30}
speed={1.2}
>
<div className="flex items-center justify-center h-full">
<span className="text-white font-medium text-xl">
{card.label}
</span>
</div>
</ElectricBorder>
</div>
))}
</InfiniteScroll>
</div>
);
}
// Vertical scrolling variant
export function VerticalScrollingCards() {
const cards = [
{ id: 1, color: "#4A9FFF", label: "Card 1" },
{ id: 2, color: "#00ff88", label: "Card 2" },
{ id: 3, color: "#BD93F9", label: "Card 3" },
];
return (
<div className="bg-[#0a0a0a] p-8">
<InfiniteScroll
direction="vertical"
height={500}
gap={24}
autoScroll={true}
autoScrollSpeed={0.3}
>
{cards.map((card) => (
<div key={card.id} className="flex justify-center">
<ElectricBorder
width={280}
height={120}
color={card.color}
borderRadius={20}
displacement={25}
>
<div className="flex items-center justify-center h-full">
<span className="text-white font-bold text-2xl">
{card.label}
</span>
</div>
</ElectricBorder>
</div>
))}
</InfiniteScroll>
</div>
);
}