Cover
A Cover component that wraps any children, providing beams and space effect, hover to reveal speed.
Build amazing websites
at warp speed
Installation
Install dependencies
npm i motion clsx tailwind-merge
Install Sparkles Dependencies
npm i @tsparticles/react @tsparticles/engine @tsparticles/slim
Add util file
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Copy the source code
components/ui/cover.tsx
"use client";
import React, { useEffect, useId, useState } from "react";
import { AnimatePresence, motion } from "motion/react";
import { useRef } from "react";
import { cn } from "@/lib/utils";
import { SparklesCore } from "@/components/ui/sparkles";
export const Cover = ({
children,
className,
}: {
children?: React.ReactNode;
className?: string;
}) => {
const [hovered, setHovered] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const [containerWidth, setContainerWidth] = useState(0);
const [beamPositions, setBeamPositions] = useState<number[]>([]);
useEffect(() => {
if (ref.current) {
setContainerWidth(ref.current?.clientWidth ?? 0);
const height = ref.current?.clientHeight ?? 0;
const numberOfBeams = Math.floor(height / 10); // Adjust the divisor to control the spacing
const positions = Array.from(
{ length: numberOfBeams },
(_, i) => (i + 1) * (height / (numberOfBeams + 1))
);
setBeamPositions(positions);
}
}, [ref.current]);
return (
<div
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
ref={ref}
className="relative hover:bg-neutral-900 group/cover inline-block dark:bg-neutral-900 bg-neutral-100 px-2 py-2 transition duration-200 rounded-sm"
>
<AnimatePresence>
{hovered && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
opacity: {
duration: 0.2,
},
}}
className="h-full w-full overflow-hidden absolute inset-0"
>
<motion.div
animate={{
translateX: ["-50%", "0%"],
}}
transition={{
translateX: {
duration: 10,
ease: "linear",
repeat: Infinity,
},
}}
className="w-[200%] h-full flex"
>
<SparklesCore
background="transparent"
minSize={0.4}
maxSize={1}
particleDensity={500}
className="w-full h-full"
particleColor="#FFFFFF"
/>
<SparklesCore
background="transparent"
minSize={0.4}
maxSize={1}
particleDensity={500}
className="w-full h-full"
particleColor="#FFFFFF"
/>
</motion.div>
</motion.div>
)}
</AnimatePresence>
{beamPositions.map((position, index) => (
<Beam
key={index}
hovered={hovered}
duration={Math.random() * 2 + 1}
delay={Math.random() * 2 + 1}
width={containerWidth}
style={{
top: `${position}px`,
}}
/>
))}
<motion.span
key={String(hovered)}
animate={{
scale: hovered ? 0.8 : 1,
x: hovered ? [0, -30, 30, -30, 30, 0] : 0,
y: hovered ? [0, 30, -30, 30, -30, 0] : 0,
}}
exit={{
filter: "none",
scale: 1,
x: 0,
y: 0,
}}
transition={{
duration: 0.2,
x: {
duration: 0.2,
repeat: Infinity,
repeatType: "loop",
},
y: {
duration: 0.2,
repeat: Infinity,
repeatType: "loop",
},
scale: {
duration: 0.2,
},
filter: {
duration: 0.2,
},
}}
className={cn(
"dark:text-white inline-block text-neutral-900 relative z-20 group-hover/cover:text-white transition duration-200",
className
)}
>
{children}
</motion.span>
<CircleIcon className="absolute -right-[2px] -top-[2px]" />
<CircleIcon className="absolute -bottom-[2px] -right-[2px]" delay={0.4} />
<CircleIcon className="absolute -left-[2px] -top-[2px]" delay={0.8} />
<CircleIcon className="absolute -bottom-[2px] -left-[2px]" delay={1.6} />
</div>
);
};
export const Beam = ({
className,
delay,
duration,
hovered,
width = 600,
...svgProps
}: {
className?: string;
delay?: number;
duration?: number;
hovered?: boolean;
width?: number;
} & React.ComponentProps<typeof motion.svg>) => {
const id = useId();
return (
<motion.svg
width={width ?? "600"}
height="1"
viewBox={`0 0 ${width ?? "600"} 1`}
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={cn("absolute inset-x-0 w-full", className)}
{...svgProps}
>
<motion.path
d={`M0 0.5H${width ?? "600"}`}
stroke={`url(#svgGradient-${id})`}
/>
<defs>
<motion.linearGradient
id={`svgGradient-${id}`}
key={String(hovered)}
gradientUnits="userSpaceOnUse"
initial={{
x1: "0%",
x2: hovered ? "-10%" : "-5%",
y1: 0,
y2: 0,
}}
animate={{
x1: "110%",
x2: hovered ? "100%" : "105%",
y1: 0,
y2: 0,
}}
transition={{
duration: hovered ? 0.5 : duration ?? 2,
ease: "linear",
repeat: Infinity,
delay: hovered ? Math.random() * (1 - 0.2) + 0.2 : 0,
repeatDelay: hovered ? Math.random() * (2 - 1) + 1 : delay ?? 1,
}}
>
<stop stopColor="#2EB9DF" stopOpacity="0" />
<stop stopColor="#3b82f6" />
<stop offset="1" stopColor="#3b82f6" stopOpacity="0" />
</motion.linearGradient>
</defs>
</motion.svg>
);
};
export const CircleIcon = ({
className,
delay,
}: {
className?: string;
delay?: number;
}) => {
return (
<div
className={cn(
`pointer-events-none animate-pulse group-hover/cover:hidden group-hover/cover:opacity-100 group h-2 w-2 rounded-full bg-neutral-600 dark:bg-white opacity-20 group-hover/cover:bg-white`,
className
)}
></div>
);
};
components/ui/sparkles.tsx
"use client";
import React, { useId, useMemo } from "react";
import { useEffect, useState } from "react";
import Particles, { initParticlesEngine } from "@tsparticles/react";
import type { Container, SingleOrMultiple } from "@tsparticles/engine";
import { loadSlim } from "@tsparticles/slim";
import { cn } from "@/lib/utils";
import { motion, useAnimation } from "motion/react";
type ParticlesProps = {
id?: string;
className?: string;
background?: string;
particleSize?: number;
minSize?: number;
maxSize?: number;
speed?: number;
particleColor?: string;
particleDensity?: number;
};
export const SparklesCore = (props: ParticlesProps) => {
const {
id,
className,
background,
minSize,
maxSize,
speed,
particleColor,
particleDensity,
} = props;
const [init, setInit] = useState(false);
useEffect(() => {
initParticlesEngine(async (engine) => {
await loadSlim(engine);
}).then(() => {
setInit(true);
});
}, []);
const controls = useAnimation();
const particlesLoaded = async (container?: Container) => {
if (container) {
controls.start({
opacity: 1,
transition: {
duration: 1,
},
});
}
};
const generatedId = useId();
return (
<motion.div animate={controls} className={cn("opacity-0", className)}>
{init && (
<Particles
id={id || generatedId}
className={cn("h-full w-full")}
particlesLoaded={particlesLoaded}
options={{
background: {
color: {
value: background || "#0d47a1",
},
},
fullScreen: {
enable: false,
zIndex: 1,
},
fpsLimit: 120,
interactivity: {
events: {
onClick: {
enable: true,
mode: "push",
},
onHover: {
enable: false,
mode: "repulse",
},
resize: true as any,
},
modes: {
push: {
quantity: 4,
},
repulse: {
distance: 200,
duration: 0.4,
},
},
},
particles: {
bounce: {
horizontal: {
value: 1,
},
vertical: {
value: 1,
},
},
collisions: {
absorb: {
speed: 2,
},
bounce: {
horizontal: {
value: 1,
},
vertical: {
value: 1,
},
},
enable: false,
maxSpeed: 50,
mode: "bounce",
overlap: {
enable: true,
retries: 0,
},
},
color: {
value: particleColor || "#ffffff",
animation: {
h: {
count: 0,
enable: false,
speed: 1,
decay: 0,
delay: 0,
sync: true,
offset: 0,
},
s: {
count: 0,
enable: false,
speed: 1,
decay: 0,
delay: 0,
sync: true,
offset: 0,
},
l: {
count: 0,
enable: false,
speed: 1,
decay: 0,
delay: 0,
sync: true,
offset: 0,
},
},
},
effect: {
close: true,
fill: true,
options: {},
type: {} as SingleOrMultiple<string> | undefined,
},
groups: {},
move: {
angle: {
offset: 0,
value: 90,
},
attract: {
distance: 200,
enable: false,
rotate: {
x: 3000,
y: 3000,
},
},
center: {
x: 50,
y: 50,
mode: "percent",
radius: 0,
},
decay: 0,
distance: {},
direction: "none",
drift: 0,
enable: true,
gravity: {
acceleration: 9.81,
enable: false,
inverse: false,
maxSpeed: 50,
},
path: {
clamp: true,
delay: {
value: 0,
},
enable: false,
options: {},
},
outModes: {
default: "out",
},
random: false,
size: false,
speed: {
min: 0.1,
max: 1,
},
spin: {
acceleration: 0,
enable: false,
},
straight: false,
trail: {
enable: false,
length: 10,
fill: {},
},
vibrate: false,
warp: false,
},
number: {
density: {
enable: true,
width: 400,
height: 400,
},
limit: {
mode: "delete",
value: 0,
},
value: particleDensity || 120,
},
opacity: {
value: {
min: 0.1,
max: 1,
},
animation: {
count: 0,
enable: true,
speed: speed || 4,
decay: 0,
delay: 0,
sync: false,
mode: "auto",
startValue: "random",
destroy: "none",
},
},
reduceDuplicates: false,
shadow: {
blur: 0,
color: {
value: "#000",
},
enable: false,
offset: {
x: 0,
y: 0,
},
},
shape: {
close: true,
fill: true,
options: {},
type: "circle",
},
size: {
value: {
min: minSize || 1,
max: maxSize || 3,
},
animation: {
count: 0,
enable: false,
speed: 5,
decay: 0,
delay: 0,
sync: false,
mode: "auto",
startValue: "random",
destroy: "none",
},
},
stroke: {
width: 0,
},
zIndex: {
value: 0,
opacityRate: 1,
sizeRate: 1,
velocityRate: 1,
},
destroy: {
bounds: {},
mode: "none",
split: {
count: 1,
factor: {
value: 3,
},
rate: {
value: {
min: 4,
max: 9,
},
},
sizeOffset: true,
},
},
roll: {
darken: {
enable: false,
value: 0,
},
enable: false,
enlighten: {
enable: false,
value: 0,
},
mode: "vertical",
speed: 25,
},
tilt: {
value: 0,
animation: {
enable: false,
speed: 0,
decay: 0,
sync: false,
},
direction: "clockwise",
enable: false,
},
twinkle: {
lines: {
enable: false,
frequency: 0.05,
opacity: 1,
},
particles: {
enable: false,
frequency: 0.05,
opacity: 1,
},
},
wobble: {
distance: 5,
enable: false,
speed: {
angle: 50,
move: 10,
},
},
life: {
count: 0,
delay: {
value: 0,
sync: false,
},
duration: {
value: 0,
sync: false,
},
},
rotate: {
value: 0,
animation: {
enable: false,
speed: 0,
decay: 0,
sync: false,
},
direction: "clockwise",
path: false,
},
orbit: {
animation: {
count: 0,
enable: false,
speed: 1,
decay: 0,
delay: 0,
sync: false,
},
enable: false,
opacity: 1,
rotation: {
value: 45,
},
width: 1,
},
links: {
blink: false,
color: {
value: "#fff",
},
consent: false,
distance: 100,
enable: false,
frequency: 1,
opacity: 1,
shadow: {
blur: 5,
color: {
value: "#000",
},
enable: false,
},
triangles: {
enable: false,
frequency: 1,
},
width: 1,
warp: false,
},
repulse: {
value: 0,
enabled: false,
distance: 1,
duration: 1,
factor: 1,
speed: 1,
},
},
detectRetina: true,
}}
/>
)}
</motion.div>
);
};
Props
Prop | Type | Default | Description |
---|---|---|---|
children | React.ReactNode | undefined | The content to be wrapped by the Cover component |
className | string | undefined | Additional CSS classes to apply to the content wrapper |
Build websites faster and 10x better than your competitors with Aceternity UI Pro
With the best in class components and templates, stand out from the crowd and get more attention to your website. Trusted by founders and entrepreneurs from all over the world.
Manu is an artist, I didn't know what I wanted when we started, but his
intuition and eye for design more than made up for it. We went from "I
want something dark theme and high...
John Ferry
President at TAC, CEO at Rogue