Aceternity UI - Komponenty React z Efektami 3D i Animacjami
Czym jest Aceternity UI?
Aceternity UI to kolekcja darmowych, open-source komponentów React zaprojektowanych z myślą o tworzeniu spektakularnych efektów wizualnych. Biblioteka oferuje gotowe do użycia komponenty z zaawansowanymi animacjami 3D, efektami parallax, świeceniami (glow), spotlight effects i wieloma innymi efektami wizualnymi, które nadają aplikacjom i stronom internetowym profesjonalny, nowoczesny wygląd.
Aceternity UI powstało jako odpowiedź na rosnące zapotrzebowanie deweloperów na łatwo dostępne, wysokiej jakości komponenty wizualne. Twórcy biblioteki zebrali najczęściej spotykane efekty z nowoczesnych stron internetowych firm technologicznych i udostępnili je w formie copy-paste komponentów, które można łatwo dostosować do własnych potrzeb.
Biblioteka jest zbudowana na fundamentach Tailwind CSS i Framer Motion, co zapewnia doskonałą wydajność animacji i łatwość customizacji. Komponenty są w pełni responsywne i wspierają tryb ciemny (dark mode) out of the box.
Dlaczego Aceternity UI?
Kluczowe zalety
- Copy-Paste Approach - Każdy komponent to gotowy kod do skopiowania
- Framer Motion - Płynne, wydajne animacje 60fps
- Tailwind CSS - Pełna kontrola nad stylami
- TypeScript - Pełne wsparcie typów
- Zero Dependencies - Tylko Tailwind i Framer Motion
- Dark Mode - Natywne wsparcie trybu ciemnego
- Responsywność - Mobile-first design
- Dokumentacja - Szczegółowe przykłady użycia
Aceternity UI vs Magic UI vs shadcn/ui
| Cecha | Aceternity UI | Magic UI | shadcn/ui |
|---|---|---|---|
| Focus | Efekty 3D/wizualne | Animacje micro | Form components |
| Animacje | Bardzo zaawansowane | Zaawansowane | Podstawowe |
| Efekty 3D | Tak, główny focus | Częściowo | Nie |
| Copy-paste | Tak | Tak | Tak |
| Tailwind | Tak | Tak | Tak |
| Framer Motion | Wymagane | Wymagane | Opcjonalne |
| Ilość komponentów | 50+ | 100+ | 50+ |
| Best for | Landing pages | Marketing | Aplikacje |
Instalacja i konfiguracja
Wymagane zależności
# Podstawowe zależności
npm install framer-motion clsx tailwind-merge
# Opcjonalne dla niektórych komponentów
npm install @tabler/icons-react
npm install simplex-noise # dla efektów particleKonfiguracja Tailwind CSS
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: 'class',
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
animation: {
spotlight: 'spotlight 2s ease .75s 1 forwards',
shimmer: 'shimmer 2s linear infinite',
'meteor-effect': 'meteor 5s linear infinite',
aurora: 'aurora 60s linear infinite',
'border-beam': 'border-beam calc(var(--duration)*1s) infinite linear',
},
keyframes: {
spotlight: {
'0%': {
opacity: 0,
transform: 'translate(-72%, -62%) scale(0.5)',
},
'100%': {
opacity: 1,
transform: 'translate(-50%,-40%) scale(1)',
},
},
shimmer: {
from: {
backgroundPosition: '0 0',
},
to: {
backgroundPosition: '-200% 0',
},
},
meteor: {
'0%': { transform: 'rotate(215deg) translateX(0)', opacity: 1 },
'70%': { opacity: 1 },
'100%': {
transform: 'rotate(215deg) translateX(-500px)',
opacity: 0,
},
},
aurora: {
from: {
backgroundPosition: '50% 50%, 50% 50%',
},
to: {
backgroundPosition: '350% 50%, 350% 50%',
},
},
'border-beam': {
'100%': {
'offset-distance': '100%',
},
},
},
},
},
plugins: [],
}Utility funkcja cn()
// lib/utils.ts
import { ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}Popularne komponenty Aceternity UI
3D Card Effect
Karta z efektem 3D follow-mouse - najbardziej ikoniczny komponent biblioteki.
// components/ui/3d-card.tsx
'use client'
import { cn } from '@/lib/utils'
import React, {
createContext,
useState,
useContext,
useRef,
useEffect,
} from 'react'
const MouseEnterContext = createContext<
[boolean, React.Dispatch<React.SetStateAction<boolean>>] | undefined
>(undefined)
export const CardContainer = ({
children,
className,
containerClassName,
}: {
children?: React.ReactNode
className?: string
containerClassName?: string
}) => {
const containerRef = useRef<HTMLDivElement>(null)
const [isMouseEntered, setIsMouseEntered] = useState(false)
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!containerRef.current) return
const { left, top, width, height } =
containerRef.current.getBoundingClientRect()
const x = (e.clientX - left - width / 2) / 25
const y = (e.clientY - top - height / 2) / 25
containerRef.current.style.transform = `rotateY(${x}deg) rotateX(${y}deg)`
}
const handleMouseEnter = () => {
setIsMouseEntered(true)
}
const handleMouseLeave = () => {
if (!containerRef.current) return
setIsMouseEntered(false)
containerRef.current.style.transform = `rotateY(0deg) rotateX(0deg)`
}
return (
<MouseEnterContext.Provider value={[isMouseEntered, setIsMouseEntered]}>
<div
className={cn(
'py-20 flex items-center justify-center',
containerClassName
)}
style={{
perspective: '1000px',
}}
>
<div
ref={containerRef}
onMouseEnter={handleMouseEnter}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
className={cn(
'flex items-center justify-center relative transition-all duration-200 ease-linear',
className
)}
style={{
transformStyle: 'preserve-3d',
}}
>
{children}
</div>
</div>
</MouseEnterContext.Provider>
)
}
export const CardBody = ({
children,
className,
}: {
children: React.ReactNode
className?: string
}) => {
return (
<div
className={cn(
'h-96 w-96 [transform-style:preserve-3d] [&>*]:[transform-style:preserve-3d]',
className
)}
>
{children}
</div>
)
}
export const CardItem = ({
as: Tag = 'div',
children,
className,
translateX = 0,
translateY = 0,
translateZ = 0,
rotateX = 0,
rotateY = 0,
rotateZ = 0,
...rest
}: {
as?: React.ElementType
children: React.ReactNode
className?: string
translateX?: number | string
translateY?: number | string
translateZ?: number | string
rotateX?: number | string
rotateY?: number | string
rotateZ?: number | string
[key: string]: unknown
}) => {
const ref = useRef<HTMLDivElement>(null)
const [isMouseEntered] = useMouseEnter()
useEffect(() => {
handleAnimations()
}, [isMouseEntered])
const handleAnimations = () => {
if (!ref.current) return
if (isMouseEntered) {
ref.current.style.transform = `translateX(${translateX}px) translateY(${translateY}px) translateZ(${translateZ}px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) rotateZ(${rotateZ}deg)`
} else {
ref.current.style.transform = `translateX(0px) translateY(0px) translateZ(0px) rotateX(0deg) rotateY(0deg) rotateZ(0deg)`
}
}
return (
<Tag
ref={ref}
className={cn('w-fit transition duration-200 ease-linear', className)}
{...rest}
>
{children}
</Tag>
)
}
export const useMouseEnter = () => {
const context = useContext(MouseEnterContext)
if (context === undefined) {
throw new Error('useMouseEnter must be used within a MouseEnterProvider')
}
return context
}Przykład użycia 3D Card:
import { CardContainer, CardBody, CardItem } from '@/components/ui/3d-card'
import Image from 'next/image'
export function ThreeDCardDemo() {
return (
<CardContainer className="inter-var">
<CardBody className="bg-gray-50 relative group/card dark:hover:shadow-2xl dark:hover:shadow-emerald-500/[0.1] dark:bg-black dark:border-white/[0.2] border-black/[0.1] w-auto sm:w-[30rem] h-auto rounded-xl p-6 border">
<CardItem
translateZ="50"
className="text-xl font-bold text-neutral-600 dark:text-white"
>
Make things float in air
</CardItem>
<CardItem
as="p"
translateZ="60"
className="text-neutral-500 text-sm max-w-sm mt-2 dark:text-neutral-300"
>
Hover over this card to unleash the power of CSS perspective
</CardItem>
<CardItem translateZ="100" className="w-full mt-4">
<Image
src="/images/product.png"
height="1000"
width="1000"
className="h-60 w-full object-cover rounded-xl group-hover/card:shadow-xl"
alt="thumbnail"
/>
</CardItem>
<div className="flex justify-between items-center mt-20">
<CardItem
translateZ={20}
as="button"
className="px-4 py-2 rounded-xl text-xs font-normal dark:text-white"
>
Try now →
</CardItem>
<CardItem
translateZ={20}
as="button"
className="px-4 py-2 rounded-xl bg-black dark:bg-white dark:text-black text-white text-xs font-bold"
>
Sign up
</CardItem>
</div>
</CardBody>
</CardContainer>
)
}Spotlight Effect
Efekt spotlight podążający za kursorem - świetny do hero sections.
// components/ui/spotlight.tsx
'use client'
import React from 'react'
import { cn } from '@/lib/utils'
type SpotlightProps = {
className?: string
fill?: string
}
export const Spotlight = ({ className, fill }: SpotlightProps) => {
return (
<svg
className={cn(
'animate-spotlight pointer-events-none absolute z-[1] h-[169%] w-[138%] lg:w-[84%] opacity-0',
className
)}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 3787 2842"
fill="none"
>
<g filter="url(#filter)">
<ellipse
cx="1924.71"
cy="273.501"
rx="1924.71"
ry="273.501"
transform="matrix(-0.822377 -0.568943 -0.568943 0.822377 3631.88 2291.09)"
fill={fill || 'white'}
fillOpacity="0.21"
></ellipse>
</g>
<defs>
<filter
id="filter"
x="0.860352"
y="0.838989"
width="3785.16"
height="2840.26"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix"></feFlood>
<feBlend
mode="normal"
in="SourceGraphic"
in2="BackgroundImageFix"
result="shape"
></feBlend>
<feGaussianBlur
stdDeviation="151"
result="effect1_foregroundBlur_1065_8"
></feGaussianBlur>
</filter>
</defs>
</svg>
)
}Przykład użycia Spotlight:
import { Spotlight } from '@/components/ui/spotlight'
export function SpotlightPreview() {
return (
<div className="h-[40rem] w-full rounded-md flex md:items-center md:justify-center bg-black/[0.96] antialiased bg-grid-white/[0.02] relative overflow-hidden">
<Spotlight
className="-top-40 left-0 md:left-60 md:-top-20"
fill="white"
/>
<div className="p-4 max-w-7xl mx-auto relative z-10 w-full pt-20 md:pt-0">
<h1 className="text-4xl md:text-7xl font-bold text-center bg-clip-text text-transparent bg-gradient-to-b from-neutral-50 to-neutral-400 bg-opacity-50">
Spotlight <br /> is the new trend.
</h1>
<p className="mt-4 font-normal text-base text-neutral-300 max-w-lg text-center mx-auto">
Spotlight effect is a great way to draw attention to a specific part
of the page. Here, we are drawing the attention towards the text
section of the page. I don't know what else to write so I'll
just keep writing gibberish.
</p>
</div>
</div>
)
}Meteors Effect
Spadające meteory w tle - efekt wizualny inspirowany kosmosem.
// components/ui/meteors.tsx
'use client'
import { cn } from '@/lib/utils'
import React from 'react'
export const Meteors = ({
number,
className,
}: {
number?: number
className?: string
}) => {
const meteors = new Array(number || 20).fill(true)
return (
<>
{meteors.map((_, idx) => (
<span
key={'meteor' + idx}
className={cn(
'animate-meteor-effect absolute top-1/2 left-1/2 h-0.5 w-0.5 rounded-[9999px] bg-slate-500 shadow-[0_0_0_1px_#ffffff10] rotate-[215deg]',
"before:content-[''] before:absolute before:top-1/2 before:-translate-y-[50%] before:w-[50px] before:h-[1px] before:bg-gradient-to-r before:from-[#64748b] before:to-transparent",
className
)}
style={{
top: 0,
left: Math.floor(Math.random() * (400 - -400) + -400) + 'px',
animationDelay: Math.random() * (0.8 - 0.2) + 0.2 + 's',
animationDuration: Math.floor(Math.random() * (10 - 2) + 2) + 's',
}}
></span>
))}
</>
)
}Przykład użycia Meteors:
import { Meteors } from '@/components/ui/meteors'
export function MeteorsDemo() {
return (
<div className="w-full relative max-w-xs">
<div className="absolute inset-0 h-full w-full bg-gradient-to-r from-blue-500 to-teal-500 transform scale-[0.80] bg-red-500 rounded-full blur-3xl" />
<div className="relative shadow-xl bg-gray-900 border border-gray-800 px-4 py-8 h-full overflow-hidden rounded-2xl flex flex-col justify-end items-start">
<div className="h-5 w-5 rounded-full border flex items-center justify-center mb-4 border-gray-500">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="h-2 w-2 text-gray-300"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 4.5l15 15m0 0V8.25m0 11.25H8.25"
/>
</svg>
</div>
<h1 className="font-bold text-xl text-white mb-4 relative z-50">
Meteors because they're cool
</h1>
<p className="font-normal text-base text-slate-500 mb-4 relative z-50">
I don't know what to write so I'll just paste something cool
here. One more sentence because lorem ipsum is boring.
</p>
<button className="border px-4 py-1 rounded-lg border-gray-500 text-gray-300">
Explore
</button>
<Meteors number={20} />
</div>
</div>
)
}Aurora Background
Animowane aurora borealis w tle - efekt zorzy polarnej.
// components/ui/aurora-background.tsx
'use client'
import { cn } from '@/lib/utils'
import React, { ReactNode } from 'react'
interface AuroraBackgroundProps extends React.HTMLProps<HTMLDivElement> {
children: ReactNode
showRadialGradient?: boolean
}
export const AuroraBackground = ({
className,
children,
showRadialGradient = true,
...props
}: AuroraBackgroundProps) => {
return (
<main>
<div
className={cn(
'relative flex flex-col h-[100vh] items-center justify-center bg-zinc-50 dark:bg-zinc-900 text-slate-950 transition-bg',
className
)}
{...props}
>
<div className="absolute inset-0 overflow-hidden">
<div
className={cn(
`
[--white-gradient:repeating-linear-gradient(100deg,var(--white)_0%,var(--white)_7%,var(--transparent)_10%,var(--transparent)_12%,var(--white)_16%)]
[--dark-gradient:repeating-linear-gradient(100deg,var(--black)_0%,var(--black)_7%,var(--transparent)_10%,var(--transparent)_12%,var(--black)_16%)]
[--aurora:repeating-linear-gradient(100deg,var(--blue-500)_10%,var(--indigo-300)_15%,var(--blue-300)_20%,var(--violet-200)_25%,var(--blue-400)_30%)]
[background-image:var(--white-gradient),var(--aurora)]
dark:[background-image:var(--dark-gradient),var(--aurora)]
[background-size:300%,_200%]
[background-position:50%_50%,50%_50%]
filter blur-[10px] invert dark:invert-0
after:content-[""] after:absolute after:inset-0 after:[background-image:var(--white-gradient),var(--aurora)]
after:dark:[background-image:var(--dark-gradient),var(--aurora)]
after:[background-size:200%,_100%]
after:animate-aurora after:[background-attachment:fixed] after:mix-blend-difference
pointer-events-none
absolute -inset-[10px] opacity-50 will-change-transform`,
showRadialGradient &&
`[mask-image:radial-gradient(ellipse_at_100%_0%,black_10%,var(--transparent)_70%)]`
)}
></div>
</div>
{children}
</div>
</main>
)
}Przykład użycia Aurora Background:
'use client'
import { motion } from 'framer-motion'
import React from 'react'
import { AuroraBackground } from '@/components/ui/aurora-background'
export function AuroraBackgroundDemo() {
return (
<AuroraBackground>
<motion.div
initial={{ opacity: 0.0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{
delay: 0.3,
duration: 0.8,
ease: 'easeInOut',
}}
className="relative flex flex-col gap-4 items-center justify-center px-4"
>
<div className="text-3xl md:text-7xl font-bold dark:text-white text-center">
Background lights are cool you know.
</div>
<div className="font-extralight text-base md:text-4xl dark:text-neutral-200 py-4">
And this, is chemical burn.
</div>
<button className="bg-black dark:bg-white rounded-full w-fit text-white dark:text-black px-4 py-2">
Debug now
</button>
</motion.div>
</AuroraBackground>
)
}Sparkles Effect
Animowane iskierki wokół elementu - subtelny efekt magii.
// components/ui/sparkles.tsx
'use client'
import React, { useId, useMemo } from 'react'
import { motion } from 'framer-motion'
import { cn } from '@/lib/utils'
interface SparkleProps {
id: string
createdAt: number
color: string
size: number
style: {
top: string
left: string
zIndex: number
}
}
interface SparklesProps {
className?: string
children: React.ReactNode
sparklesCount?: number
colors?: {
first: string
second: string
}
}
const DEFAULT_SPARKLE_COLORS = {
first: '#9E7AFF',
second: '#FE8BBB',
}
const generateSparkle = (colors = DEFAULT_SPARKLE_COLORS): SparkleProps => {
return {
id: String(Math.random()),
createdAt: Date.now(),
color: Math.random() > 0.5 ? colors.first : colors.second,
size: Math.random() * 10 + 10,
style: {
top: Math.random() * 100 + '%',
left: Math.random() * 100 + '%',
zIndex: 2,
},
}
}
const Sparkle = ({ color, size, style }: Omit<SparkleProps, 'id' | 'createdAt'>) => {
return (
<motion.svg
width={size}
height={size}
viewBox="0 0 160 160"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={style}
className="absolute pointer-events-none"
initial={{ scale: 0, rotate: 0 }}
animate={{
scale: [0, 1, 0],
rotate: [0, 180],
}}
transition={{
duration: 1.5,
repeat: Infinity,
repeatDelay: Math.random() * 2,
}}
>
<path
d="M80 0C80 0 84.2846 41.2925 101.496 58.504C118.707 75.7154 160 80 160 80C160 80 118.707 84.2846 101.496 101.496C84.2846 118.707 80 160 80 160C80 160 75.7154 118.707 58.504 101.496C41.2925 84.2846 0 80 0 80C0 80 41.2925 75.7154 58.504 58.504C75.7154 41.2925 80 0 80 0Z"
fill={color}
/>
</motion.svg>
)
}
export const Sparkles: React.FC<SparklesProps> = ({
children,
className,
sparklesCount = 10,
colors = DEFAULT_SPARKLE_COLORS,
}) => {
const sparkles = useMemo(() => {
return Array.from({ length: sparklesCount }, () => generateSparkle(colors))
}, [sparklesCount, colors])
return (
<span className={cn('relative inline-block', className)}>
{sparkles.map((sparkle) => (
<Sparkle
key={sparkle.id}
color={sparkle.color}
size={sparkle.size}
style={sparkle.style}
/>
))}
<span className="relative z-10">{children}</span>
</span>
)
}Przykład użycia Sparkles:
import { Sparkles } from '@/components/ui/sparkles'
export function SparklesPreview() {
return (
<div className="h-40 w-full bg-black flex items-center justify-center">
<Sparkles
sparklesCount={12}
colors={{
first: '#A07CFE',
second: '#FE8FB5',
}}
>
<span className="text-4xl font-bold text-white">
Magical Text
</span>
</Sparkles>
</div>
)
}Lamp Effect
Świecąca lampa z animowanym światłem - dramatyczny efekt hero.
// components/ui/lamp.tsx
'use client'
import React from 'react'
import { motion } from 'framer-motion'
import { cn } from '@/lib/utils'
export const LampContainer = ({
children,
className,
}: {
children: React.ReactNode
className?: string
}) => {
return (
<div
className={cn(
'relative flex min-h-screen flex-col items-center justify-center overflow-hidden bg-slate-950 w-full rounded-md z-0',
className
)}
>
<div className="relative flex w-full flex-1 scale-y-125 items-center justify-center isolate z-0">
<motion.div
initial={{ opacity: 0.5, width: '15rem' }}
whileInView={{ opacity: 1, width: '30rem' }}
transition={{
delay: 0.3,
duration: 0.8,
ease: 'easeInOut',
}}
style={{
backgroundImage: `conic-gradient(var(--conic-position), var(--tw-gradient-stops))`,
}}
className="absolute inset-auto right-1/2 h-56 overflow-visible w-[30rem] bg-gradient-conic from-cyan-500 via-transparent to-transparent text-white [--conic-position:from_70deg_at_center_top]"
>
<div className="absolute w-[100%] left-0 bg-slate-950 h-40 bottom-0 z-20 [mask-image:linear-gradient(to_top,white,transparent)]" />
<div className="absolute w-40 h-[100%] left-0 bg-slate-950 bottom-0 z-20 [mask-image:linear-gradient(to_right,white,transparent)]" />
</motion.div>
<motion.div
initial={{ opacity: 0.5, width: '15rem' }}
whileInView={{ opacity: 1, width: '30rem' }}
transition={{
delay: 0.3,
duration: 0.8,
ease: 'easeInOut',
}}
style={{
backgroundImage: `conic-gradient(var(--conic-position), var(--tw-gradient-stops))`,
}}
className="absolute inset-auto left-1/2 h-56 w-[30rem] bg-gradient-conic from-transparent via-transparent to-cyan-500 text-white [--conic-position:from_290deg_at_center_top]"
>
<div className="absolute w-40 h-[100%] right-0 bg-slate-950 bottom-0 z-20 [mask-image:linear-gradient(to_left,white,transparent)]" />
<div className="absolute w-[100%] right-0 bg-slate-950 h-40 bottom-0 z-20 [mask-image:linear-gradient(to_top,white,transparent)]" />
</motion.div>
<div className="absolute top-1/2 h-48 w-full translate-y-12 scale-x-150 bg-slate-950 blur-2xl"></div>
<div className="absolute top-1/2 z-50 h-48 w-full bg-transparent opacity-10 backdrop-blur-md"></div>
<div className="absolute inset-auto z-50 h-36 w-[28rem] -translate-y-1/2 rounded-full bg-cyan-500 opacity-50 blur-3xl"></div>
<motion.div
initial={{ width: '8rem' }}
whileInView={{ width: '16rem' }}
transition={{
delay: 0.3,
duration: 0.8,
ease: 'easeInOut',
}}
className="absolute inset-auto z-30 h-36 w-64 -translate-y-[6rem] rounded-full bg-cyan-400 blur-2xl"
></motion.div>
<motion.div
initial={{ width: '15rem' }}
whileInView={{ width: '30rem' }}
transition={{
delay: 0.3,
duration: 0.8,
ease: 'easeInOut',
}}
className="absolute inset-auto z-50 h-0.5 w-[30rem] -translate-y-[7rem] bg-cyan-400"
></motion.div>
<div className="absolute inset-auto z-40 h-44 w-full -translate-y-[12.5rem] bg-slate-950"></div>
</div>
<div className="relative z-50 flex -translate-y-80 flex-col items-center px-5">
{children}
</div>
</div>
)
}Przykład użycia Lamp Effect:
'use client'
import React from 'react'
import { motion } from 'framer-motion'
import { LampContainer } from '@/components/ui/lamp'
export function LampDemo() {
return (
<LampContainer>
<motion.h1
initial={{ opacity: 0.5, y: 100 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{
delay: 0.3,
duration: 0.8,
ease: 'easeInOut',
}}
className="mt-8 bg-gradient-to-br from-slate-300 to-slate-500 py-4 bg-clip-text text-center text-4xl font-medium tracking-tight text-transparent md:text-7xl"
>
Build lamps <br /> the right way
</motion.h1>
</LampContainer>
)
}Background Beams
Animowane wiązki świetlne w tle.
// components/ui/background-beams.tsx
'use client'
import React from 'react'
import { motion } from 'framer-motion'
import { cn } from '@/lib/utils'
export const BackgroundBeams = ({ className }: { className?: string }) => {
const paths = [
'M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875',
'M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867',
'M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859',
'M-359 -213C-359 -213 -291 192 173 319C637 446 705 851 705 851',
'M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843',
'M-345 -229C-345 -229 -277 176 187 303C651 430 719 835 719 835',
'M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827',
'M-331 -245C-331 -245 -263 160 201 287C665 414 733 819 733 819',
'M-324 -253C-324 -253 -256 152 208 279C672 406 740 811 740 811',
'M-317 -261C-317 -261 -249 144 215 271C679 398 747 803 747 803',
]
return (
<div
className={cn(
'absolute h-full w-full inset-0 [mask-size:40px] [mask-repeat:no-repeat] flex items-center justify-center',
className
)}
>
<svg
className="z-0 h-full w-full pointer-events-none absolute"
width="100%"
height="100%"
viewBox="0 0 696 316"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{paths.map((path, index) => (
<motion.path
key={`path-` + index}
d={path}
stroke={`url(#linearGradient-${index})`}
strokeOpacity="0.4"
strokeWidth="0.5"
></motion.path>
))}
<defs>
{paths.map((_, index) => (
<motion.linearGradient
id={`linearGradient-${index}`}
key={`gradient-${index}`}
initial={{
x1: '0%',
x2: '0%',
y1: '0%',
y2: '0%',
}}
animate={{
x1: ['0%', '100%'],
x2: ['0%', '95%'],
y1: ['0%', '100%'],
y2: ['0%', `${93 + Math.random() * 8}%`],
}}
transition={{
duration: Math.random() * 10 + 10,
ease: 'easeInOut',
repeat: Infinity,
delay: Math.random() * 10,
}}
>
<stop stopColor="#18CCFC" stopOpacity="0"></stop>
<stop stopColor="#18CCFC"></stop>
<stop offset="32.5%" stopColor="#6344F5"></stop>
<stop offset="100%" stopColor="#AE48FF" stopOpacity="0"></stop>
</motion.linearGradient>
))}
</defs>
</svg>
</div>
)
}Text Generate Effect
Animowane generowanie tekstu litera po literze.
// components/ui/text-generate-effect.tsx
'use client'
import { useEffect } from 'react'
import { motion, stagger, useAnimate } from 'framer-motion'
import { cn } from '@/lib/utils'
export const TextGenerateEffect = ({
words,
className,
filter = true,
duration = 0.5,
}: {
words: string
className?: string
filter?: boolean
duration?: number
}) => {
const [scope, animate] = useAnimate()
const wordsArray = words.split(' ')
useEffect(() => {
animate(
'span',
{
opacity: 1,
filter: filter ? 'blur(0px)' : 'none',
},
{
duration: duration ? duration : 1,
delay: stagger(0.2),
}
)
}, [scope.current])
const renderWords = () => {
return (
<motion.div ref={scope}>
{wordsArray.map((word, idx) => {
return (
<motion.span
key={word + idx}
className="dark:text-white text-black opacity-0"
style={{
filter: filter ? 'blur(10px)' : 'none',
}}
>
{word}{' '}
</motion.span>
)
})}
</motion.div>
)
}
return (
<div className={cn('font-bold', className)}>
<div className="mt-4">
<div className="dark:text-white text-black text-2xl leading-snug tracking-wide">
{renderWords()}
</div>
</div>
</div>
)
}Przykład użycia Text Generate Effect:
import { TextGenerateEffect } from '@/components/ui/text-generate-effect'
const words = `Oxygen gets you high. In a catastrophic emergency, we're taking giant, panicked breaths. Suddenly you become euphoric, docile. You accept your fate.`
export function TextGenerateEffectDemo() {
return <TextGenerateEffect words={words} />
}Zaawansowane komponenty
Animated Tabs
// components/ui/animated-tabs.tsx
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
import { cn } from '@/lib/utils'
type Tab = {
title: string
value: string
content?: string | React.ReactNode
}
export const Tabs = ({
tabs: propTabs,
containerClassName,
activeTabClassName,
tabClassName,
contentClassName,
}: {
tabs: Tab[]
containerClassName?: string
activeTabClassName?: string
tabClassName?: string
contentClassName?: string
}) => {
const [active, setActive] = useState<Tab>(propTabs[0])
const [tabs, setTabs] = useState<Tab[]>(propTabs)
const moveSelectedTabToTop = (idx: number) => {
const newTabs = [...propTabs]
const selectedTab = newTabs.splice(idx, 1)
newTabs.unshift(selectedTab[0])
setTabs(newTabs)
setActive(newTabs[0])
}
const [hovering, setHovering] = useState(false)
return (
<>
<div
className={cn(
'flex flex-row items-center justify-start [perspective:1000px] relative overflow-auto sm:overflow-visible no-visible-scrollbar max-w-full w-full',
containerClassName
)}
>
{propTabs.map((tab, idx) => (
<button
key={tab.title}
onClick={() => {
moveSelectedTabToTop(idx)
}}
onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
className={cn('relative px-4 py-2 rounded-full', tabClassName)}
style={{
transformStyle: 'preserve-3d',
}}
>
{active.value === tab.value && (
<motion.div
layoutId="clickedbutton"
transition={{ type: 'spring', bounce: 0.3, duration: 0.6 }}
className={cn(
'absolute inset-0 bg-gray-200 dark:bg-zinc-800 rounded-full',
activeTabClassName
)}
/>
)}
<span className="relative block text-black dark:text-white">
{tab.title}
</span>
</button>
))}
</div>
<FadeInDiv
tabs={tabs}
active={active}
key={active.value}
hovering={hovering}
className={cn('mt-32', contentClassName)}
/>
</>
)
}
export const FadeInDiv = ({
className,
tabs,
hovering,
}: {
className?: string
key?: string
tabs: Tab[]
active: Tab
hovering?: boolean
}) => {
const isActive = (tab: Tab) => {
return tab.value === tabs[0].value
}
return (
<div className="relative w-full h-full">
{tabs.map((tab, idx) => (
<motion.div
key={tab.value}
layoutId={tab.value}
style={{
scale: 1 - idx * 0.1,
top: hovering ? idx * -50 : 0,
zIndex: -idx,
opacity: idx < 3 ? 1 - idx * 0.1 : 0,
}}
animate={{
y: isActive(tab) ? [0, 40, 0] : 0,
}}
className={cn('w-full h-full absolute top-0 left-0', className)}
>
{tab.content}
</motion.div>
))}
</div>
)
}Infinite Moving Cards
Nieskończenie przewijające się karty - idealne do testimoniali.
// components/ui/infinite-moving-cards.tsx
'use client'
import { cn } from '@/lib/utils'
import React, { useEffect, useState } from 'react'
export const InfiniteMovingCards = ({
items,
direction = 'left',
speed = 'fast',
pauseOnHover = true,
className,
}: {
items: {
quote: string
name: string
title: string
}[]
direction?: 'left' | 'right'
speed?: 'fast' | 'normal' | 'slow'
pauseOnHover?: boolean
className?: string
}) => {
const containerRef = React.useRef<HTMLDivElement>(null)
const scrollerRef = React.useRef<HTMLUListElement>(null)
useEffect(() => {
addAnimation()
}, [])
const [start, setStart] = useState(false)
function addAnimation() {
if (containerRef.current && scrollerRef.current) {
const scrollerContent = Array.from(scrollerRef.current.children)
scrollerContent.forEach((item) => {
const duplicatedItem = item.cloneNode(true)
if (scrollerRef.current) {
scrollerRef.current.appendChild(duplicatedItem)
}
})
getDirection()
getSpeed()
setStart(true)
}
}
const getDirection = () => {
if (containerRef.current) {
if (direction === 'left') {
containerRef.current.style.setProperty(
'--animation-direction',
'forwards'
)
} else {
containerRef.current.style.setProperty(
'--animation-direction',
'reverse'
)
}
}
}
const getSpeed = () => {
if (containerRef.current) {
if (speed === 'fast') {
containerRef.current.style.setProperty('--animation-duration', '20s')
} else if (speed === 'normal') {
containerRef.current.style.setProperty('--animation-duration', '40s')
} else {
containerRef.current.style.setProperty('--animation-duration', '80s')
}
}
}
return (
<div
ref={containerRef}
className={cn(
'scroller relative z-20 max-w-7xl overflow-hidden [mask-image:linear-gradient(to_right,transparent,white_20%,white_80%,transparent)]',
className
)}
>
<ul
ref={scrollerRef}
className={cn(
'flex min-w-full shrink-0 gap-4 py-4 w-max flex-nowrap',
start && 'animate-scroll',
pauseOnHover && 'hover:[animation-play-state:paused]'
)}
>
{items.map((item, idx) => (
<li
className="w-[350px] max-w-full relative rounded-2xl border border-b-0 flex-shrink-0 border-slate-700 px-8 py-6 md:w-[450px]"
style={{
background:
'linear-gradient(180deg, var(--slate-800), var(--slate-900)',
}}
key={item.name + idx}
>
<blockquote>
<div
aria-hidden="true"
className="user-select-none -z-1 pointer-events-none absolute -left-0.5 -top-0.5 h-[calc(100%_+_4px)] w-[calc(100%_+_4px)]"
></div>
<span className="relative z-20 text-sm leading-[1.6] text-gray-100 font-normal">
{item.quote}
</span>
<div className="relative z-20 mt-6 flex flex-row items-center">
<span className="flex flex-col gap-1">
<span className="text-sm leading-[1.6] text-gray-400 font-normal">
{item.name}
</span>
<span className="text-sm leading-[1.6] text-gray-400 font-normal">
{item.title}
</span>
</span>
</div>
</blockquote>
</li>
))}
</ul>
</div>
)
}Tworzenie kompletnej strony z Aceternity UI
Przykład Landing Page
// app/page.tsx
import { Spotlight } from '@/components/ui/spotlight'
import { TextGenerateEffect } from '@/components/ui/text-generate-effect'
import { CardContainer, CardBody, CardItem } from '@/components/ui/3d-card'
import { InfiniteMovingCards } from '@/components/ui/infinite-moving-cards'
import { Meteors } from '@/components/ui/meteors'
const testimonials = [
{
quote: "This product changed the way we work. Absolutely amazing experience!",
name: "Sarah Johnson",
title: "CEO at TechCorp",
},
{
quote: "Best investment we've made this year. The results speak for themselves.",
name: "Michael Chen",
title: "CTO at StartupXYZ",
},
{
quote: "Incredible customer support and outstanding product quality.",
name: "Emily Williams",
title: "Product Manager at InnovateCo",
},
]
const features = [
{
title: "Lightning Fast",
description: "Optimized for speed with cutting-edge technology",
icon: "⚡",
},
{
title: "Secure by Default",
description: "Enterprise-grade security built into every feature",
icon: "🔒",
},
{
title: "AI Powered",
description: "Smart automation that learns and adapts to your needs",
icon: "🤖",
},
]
export default function LandingPage() {
return (
<main className="min-h-screen bg-black/[0.96] antialiased bg-grid-white/[0.02]">
{/* Hero Section with Spotlight */}
<section className="h-screen w-full flex md:items-center md:justify-center relative overflow-hidden">
<Spotlight
className="-top-40 left-0 md:left-60 md:-top-20"
fill="white"
/>
<div className="p-4 max-w-7xl mx-auto relative z-10 w-full pt-20 md:pt-0">
<h1 className="text-4xl md:text-7xl font-bold text-center bg-clip-text text-transparent bg-gradient-to-b from-neutral-50 to-neutral-400">
Build Amazing
<br />
Products Faster
</h1>
<TextGenerateEffect
words="Transform your ideas into reality with our cutting-edge platform.
Start building today and see results tomorrow."
className="mt-8 text-center max-w-2xl mx-auto"
/>
<div className="flex gap-4 justify-center mt-10">
<button className="px-8 py-3 rounded-full bg-white text-black font-semibold hover:bg-gray-200 transition">
Get Started
</button>
<button className="px-8 py-3 rounded-full border border-white/20 text-white hover:bg-white/10 transition">
Learn More
</button>
</div>
</div>
</section>
{/* Features Section with 3D Cards */}
<section className="py-20 px-4">
<h2 className="text-3xl md:text-5xl font-bold text-center text-white mb-16">
Powerful Features
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-6xl mx-auto">
{features.map((feature, idx) => (
<CardContainer key={idx} className="inter-var">
<CardBody className="bg-gray-900 relative group/card border-white/[0.1] w-full h-auto rounded-xl p-6 border">
<CardItem
translateZ="50"
className="text-4xl mb-4"
>
{feature.icon}
</CardItem>
<CardItem
translateZ="60"
className="text-xl font-bold text-white"
>
{feature.title}
</CardItem>
<CardItem
as="p"
translateZ="80"
className="text-neutral-400 text-sm mt-2"
>
{feature.description}
</CardItem>
</CardBody>
</CardContainer>
))}
</div>
</section>
{/* Testimonials Section */}
<section className="py-20 relative">
<h2 className="text-3xl md:text-5xl font-bold text-center text-white mb-16">
What Our Customers Say
</h2>
<InfiniteMovingCards
items={testimonials}
direction="right"
speed="slow"
/>
</section>
{/* CTA Section with Meteors */}
<section className="py-20 px-4 relative overflow-hidden">
<div className="max-w-2xl mx-auto relative">
<div className="absolute inset-0 h-full w-full bg-gradient-to-r from-blue-500 to-teal-500 transform scale-[0.80] rounded-full blur-3xl" />
<div className="relative shadow-xl bg-gray-900 border border-gray-800 px-8 py-12 rounded-2xl overflow-hidden">
<h2 className="text-3xl font-bold text-white text-center mb-4">
Ready to Get Started?
</h2>
<p className="text-neutral-400 text-center mb-8">
Join thousands of satisfied customers today.
</p>
<div className="flex justify-center">
<button className="px-8 py-3 rounded-full bg-gradient-to-r from-blue-500 to-teal-500 text-white font-semibold hover:opacity-90 transition">
Start Free Trial
</button>
</div>
<Meteors number={20} />
</div>
</div>
</section>
</main>
)
}Best practices
1. Optymalizacja wydajności
// Lazy loading komponentów wizualnych
import dynamic from 'next/dynamic'
const Spotlight = dynamic(
() => import('@/components/ui/spotlight').then((mod) => mod.Spotlight),
{ ssr: false }
)
const Meteors = dynamic(
() => import('@/components/ui/meteors').then((mod) => mod.Meteors),
{ ssr: false }
)2. Responsywność
// Dostosowanie efektów do urządzeń mobilnych
export function ResponsiveSpotlight() {
const isMobile = useMediaQuery('(max-width: 768px)')
return (
<div className="relative">
{/* Spotlight tylko na desktop */}
{!isMobile && (
<Spotlight className="-top-40 left-0 md:left-60 md:-top-20" fill="white" />
)}
<div className="relative z-10">
{/* Content */}
</div>
</div>
)
}3. Accessibility
// Dodanie reduced motion support
const prefersReducedMotion = usePrefersReducedMotion()
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: prefersReducedMotion ? 0 : 0.8,
}}
>
{children}
</motion.div>FAQ - Najczęściej zadawane pytania
Czy Aceternity UI jest darmowe?
Tak, Aceternity UI jest całkowicie darmowe i open-source. Wszystkie komponenty można kopiować i używać w projektach komercyjnych bez żadnych opłat.
Jakie są wymagania do użycia Aceternity UI?
Potrzebujesz projektu React z Tailwind CSS i Framer Motion. Komponenty są zaprojektowane dla Next.js, ale działają też z Create React App i Vite.
Czy komponenty działają z Server Components?
Większość komponentów Aceternity UI wymaga 'use client' ze względu na animacje Framer Motion. Możesz je importować do Server Components jako Client Component wrapper.
Jak dostosować kolory i style?
Komponenty używają Tailwind CSS, więc możesz łatwo modyfikować klasy bezpośrednio w kodzie. Kolory są często definiowane jako CSS variables lub gradienty.
Czy mogę używać Aceternity UI z TypeScript?
Tak, wszystkie komponenty mają pełne wsparcie TypeScript z typami props.
Jak rozwiązać problemy z animacjami?
Upewnij się, że Framer Motion jest zainstalowany i że komponent ma 'use client' directive. Sprawdź też czy Tailwind config zawiera potrzebne keyframes.
Czy komponenty są responsywne?
Tak, komponenty są zaprojektowane mobile-first z Tailwind breakpoints. Niektóre efekty 3D mogą być wyłączone na mobile dla wydajności.
Cennik
| Plan | Cena | Zawartość |
|---|---|---|
| Open Source | Bezpłatne | Wszystkie komponenty, copy-paste |
| Pro Templates | $49-199 | Gotowe szablony landing pages |
| Custom Development | Na zapytanie | Dedykowane komponenty |
Podsumowanie
Aceternity UI to potężna kolekcja komponentów React z efektami 3D i animacjami:
- 50+ komponentów z efektami wizualnymi
- Framer Motion dla płynnych animacji 60fps
- Tailwind CSS dla łatwej customizacji
- TypeScript z pełnym wsparciem typów
- Copy-paste approach bez dodatkowych zależności
- Dark mode out of the box
Idealne do tworzenia spektakularnych landing pages i stron marketingowych.
Aceternity UI - React components with 3D effects and animations
What is Aceternity UI?
Aceternity UI is a collection of free, open-source React components designed for creating spectacular visual effects. The library offers ready-to-use components with advanced 3D animations, parallax effects, glows, spotlight effects, and many other visual effects that give applications and websites a professional, modern look.
Aceternity UI was created in response to the growing demand from developers for easily accessible, high-quality visual components. The creators of the library gathered the most commonly seen effects from modern tech company websites and made them available as copy-paste components that can be easily adapted to your own needs.
The library is built on the foundations of Tailwind CSS and Framer Motion, which ensures excellent animation performance and ease of customization. The components are fully responsive and support dark mode out of the box.
Why Aceternity UI?
Key advantages
- Copy-Paste Approach - Every component is ready-made code to copy
- Framer Motion - Smooth, performant 60fps animations
- Tailwind CSS - Full control over styles
- TypeScript - Full type support
- Zero Dependencies - Only Tailwind and Framer Motion
- Dark Mode - Native dark mode support
- Responsiveness - Mobile-first design
- Documentation - Detailed usage examples
Aceternity UI vs Magic UI vs shadcn/ui
| Feature | Aceternity UI | Magic UI | shadcn/ui |
|---|---|---|---|
| Focus | 3D/visual effects | Micro animations | Form components |
| Animations | Very advanced | Advanced | Basic |
| 3D Effects | Yes, main focus | Partially | No |
| Copy-paste | Yes | Yes | Yes |
| Tailwind | Yes | Yes | Yes |
| Framer Motion | Required | Required | Optional |
| Number of components | 50+ | 100+ | 50+ |
| Best for | Landing pages | Marketing | Applications |
Installation and configuration
Required dependencies
npm install framer-motion clsx tailwind-merge
npm install @tabler/icons-react
npm install simplex-noiseTailwind CSS configuration
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: 'class',
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
animation: {
spotlight: 'spotlight 2s ease .75s 1 forwards',
shimmer: 'shimmer 2s linear infinite',
'meteor-effect': 'meteor 5s linear infinite',
aurora: 'aurora 60s linear infinite',
'border-beam': 'border-beam calc(var(--duration)*1s) infinite linear',
},
keyframes: {
spotlight: {
'0%': {
opacity: 0,
transform: 'translate(-72%, -62%) scale(0.5)',
},
'100%': {
opacity: 1,
transform: 'translate(-50%,-40%) scale(1)',
},
},
shimmer: {
from: {
backgroundPosition: '0 0',
},
to: {
backgroundPosition: '-200% 0',
},
},
meteor: {
'0%': { transform: 'rotate(215deg) translateX(0)', opacity: 1 },
'70%': { opacity: 1 },
'100%': {
transform: 'rotate(215deg) translateX(-500px)',
opacity: 0,
},
},
aurora: {
from: {
backgroundPosition: '50% 50%, 50% 50%',
},
to: {
backgroundPosition: '350% 50%, 350% 50%',
},
},
'border-beam': {
'100%': {
'offset-distance': '100%',
},
},
},
},
},
plugins: [],
}The cn() utility function
// lib/utils.ts
import { ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}Popular Aceternity UI components
3D Card Effect
A card with a 3D follow-mouse effect - the most iconic component of the library.
// components/ui/3d-card.tsx
'use client'
import { cn } from '@/lib/utils'
import React, {
createContext,
useState,
useContext,
useRef,
useEffect,
} from 'react'
const MouseEnterContext = createContext<
[boolean, React.Dispatch<React.SetStateAction<boolean>>] | undefined
>(undefined)
export const CardContainer = ({
children,
className,
containerClassName,
}: {
children?: React.ReactNode
className?: string
containerClassName?: string
}) => {
const containerRef = useRef<HTMLDivElement>(null)
const [isMouseEntered, setIsMouseEntered] = useState(false)
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!containerRef.current) return
const { left, top, width, height } =
containerRef.current.getBoundingClientRect()
const x = (e.clientX - left - width / 2) / 25
const y = (e.clientY - top - height / 2) / 25
containerRef.current.style.transform = `rotateY(${x}deg) rotateX(${y}deg)`
}
const handleMouseEnter = () => {
setIsMouseEntered(true)
}
const handleMouseLeave = () => {
if (!containerRef.current) return
setIsMouseEntered(false)
containerRef.current.style.transform = `rotateY(0deg) rotateX(0deg)`
}
return (
<MouseEnterContext.Provider value={[isMouseEntered, setIsMouseEntered]}>
<div
className={cn(
'py-20 flex items-center justify-center',
containerClassName
)}
style={{
perspective: '1000px',
}}
>
<div
ref={containerRef}
onMouseEnter={handleMouseEnter}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
className={cn(
'flex items-center justify-center relative transition-all duration-200 ease-linear',
className
)}
style={{
transformStyle: 'preserve-3d',
}}
>
{children}
</div>
</div>
</MouseEnterContext.Provider>
)
}
export const CardBody = ({
children,
className,
}: {
children: React.ReactNode
className?: string
}) => {
return (
<div
className={cn(
'h-96 w-96 [transform-style:preserve-3d] [&>*]:[transform-style:preserve-3d]',
className
)}
>
{children}
</div>
)
}
export const CardItem = ({
as: Tag = 'div',
children,
className,
translateX = 0,
translateY = 0,
translateZ = 0,
rotateX = 0,
rotateY = 0,
rotateZ = 0,
...rest
}: {
as?: React.ElementType
children: React.ReactNode
className?: string
translateX?: number | string
translateY?: number | string
translateZ?: number | string
rotateX?: number | string
rotateY?: number | string
rotateZ?: number | string
[key: string]: unknown
}) => {
const ref = useRef<HTMLDivElement>(null)
const [isMouseEntered] = useMouseEnter()
useEffect(() => {
handleAnimations()
}, [isMouseEntered])
const handleAnimations = () => {
if (!ref.current) return
if (isMouseEntered) {
ref.current.style.transform = `translateX(${translateX}px) translateY(${translateY}px) translateZ(${translateZ}px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) rotateZ(${rotateZ}deg)`
} else {
ref.current.style.transform = `translateX(0px) translateY(0px) translateZ(0px) rotateX(0deg) rotateY(0deg) rotateZ(0deg)`
}
}
return (
<Tag
ref={ref}
className={cn('w-fit transition duration-200 ease-linear', className)}
{...rest}
>
{children}
</Tag>
)
}
export const useMouseEnter = () => {
const context = useContext(MouseEnterContext)
if (context === undefined) {
throw new Error('useMouseEnter must be used within a MouseEnterProvider')
}
return context
}3D Card usage example:
import { CardContainer, CardBody, CardItem } from '@/components/ui/3d-card'
import Image from 'next/image'
export function ThreeDCardDemo() {
return (
<CardContainer className="inter-var">
<CardBody className="bg-gray-50 relative group/card dark:hover:shadow-2xl dark:hover:shadow-emerald-500/[0.1] dark:bg-black dark:border-white/[0.2] border-black/[0.1] w-auto sm:w-[30rem] h-auto rounded-xl p-6 border">
<CardItem
translateZ="50"
className="text-xl font-bold text-neutral-600 dark:text-white"
>
Make things float in air
</CardItem>
<CardItem
as="p"
translateZ="60"
className="text-neutral-500 text-sm max-w-sm mt-2 dark:text-neutral-300"
>
Hover over this card to unleash the power of CSS perspective
</CardItem>
<CardItem translateZ="100" className="w-full mt-4">
<Image
src="/images/product.png"
height="1000"
width="1000"
className="h-60 w-full object-cover rounded-xl group-hover/card:shadow-xl"
alt="thumbnail"
/>
</CardItem>
<div className="flex justify-between items-center mt-20">
<CardItem
translateZ={20}
as="button"
className="px-4 py-2 rounded-xl text-xs font-normal dark:text-white"
>
Try now →
</CardItem>
<CardItem
translateZ={20}
as="button"
className="px-4 py-2 rounded-xl bg-black dark:bg-white dark:text-black text-white text-xs font-bold"
>
Sign up
</CardItem>
</div>
</CardBody>
</CardContainer>
)
}Spotlight Effect
A cursor-following spotlight effect - great for hero sections.
// components/ui/spotlight.tsx
'use client'
import React from 'react'
import { cn } from '@/lib/utils'
type SpotlightProps = {
className?: string
fill?: string
}
export const Spotlight = ({ className, fill }: SpotlightProps) => {
return (
<svg
className={cn(
'animate-spotlight pointer-events-none absolute z-[1] h-[169%] w-[138%] lg:w-[84%] opacity-0',
className
)}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 3787 2842"
fill="none"
>
<g filter="url(#filter)">
<ellipse
cx="1924.71"
cy="273.501"
rx="1924.71"
ry="273.501"
transform="matrix(-0.822377 -0.568943 -0.568943 0.822377 3631.88 2291.09)"
fill={fill || 'white'}
fillOpacity="0.21"
></ellipse>
</g>
<defs>
<filter
id="filter"
x="0.860352"
y="0.838989"
width="3785.16"
height="2840.26"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix"></feFlood>
<feBlend
mode="normal"
in="SourceGraphic"
in2="BackgroundImageFix"
result="shape"
></feBlend>
<feGaussianBlur
stdDeviation="151"
result="effect1_foregroundBlur_1065_8"
></feGaussianBlur>
</filter>
</defs>
</svg>
)
}Spotlight usage example:
import { Spotlight } from '@/components/ui/spotlight'
export function SpotlightPreview() {
return (
<div className="h-[40rem] w-full rounded-md flex md:items-center md:justify-center bg-black/[0.96] antialiased bg-grid-white/[0.02] relative overflow-hidden">
<Spotlight
className="-top-40 left-0 md:left-60 md:-top-20"
fill="white"
/>
<div className="p-4 max-w-7xl mx-auto relative z-10 w-full pt-20 md:pt-0">
<h1 className="text-4xl md:text-7xl font-bold text-center bg-clip-text text-transparent bg-gradient-to-b from-neutral-50 to-neutral-400 bg-opacity-50">
Spotlight <br /> is the new trend.
</h1>
<p className="mt-4 font-normal text-base text-neutral-300 max-w-lg text-center mx-auto">
Spotlight effect is a great way to draw attention to a specific part
of the page. Here, we are drawing the attention towards the text
section of the page. I don't know what else to write so I'll
just keep writing gibberish.
</p>
</div>
</div>
)
}Meteors Effect
Falling meteors in the background - a space-inspired visual effect.
// components/ui/meteors.tsx
'use client'
import { cn } from '@/lib/utils'
import React from 'react'
export const Meteors = ({
number,
className,
}: {
number?: number
className?: string
}) => {
const meteors = new Array(number || 20).fill(true)
return (
<>
{meteors.map((_, idx) => (
<span
key={'meteor' + idx}
className={cn(
'animate-meteor-effect absolute top-1/2 left-1/2 h-0.5 w-0.5 rounded-[9999px] bg-slate-500 shadow-[0_0_0_1px_#ffffff10] rotate-[215deg]',
"before:content-[''] before:absolute before:top-1/2 before:-translate-y-[50%] before:w-[50px] before:h-[1px] before:bg-gradient-to-r before:from-[#64748b] before:to-transparent",
className
)}
style={{
top: 0,
left: Math.floor(Math.random() * (400 - -400) + -400) + 'px',
animationDelay: Math.random() * (0.8 - 0.2) + 0.2 + 's',
animationDuration: Math.floor(Math.random() * (10 - 2) + 2) + 's',
}}
></span>
))}
</>
)
}Meteors usage example:
import { Meteors } from '@/components/ui/meteors'
export function MeteorsDemo() {
return (
<div className="w-full relative max-w-xs">
<div className="absolute inset-0 h-full w-full bg-gradient-to-r from-blue-500 to-teal-500 transform scale-[0.80] bg-red-500 rounded-full blur-3xl" />
<div className="relative shadow-xl bg-gray-900 border border-gray-800 px-4 py-8 h-full overflow-hidden rounded-2xl flex flex-col justify-end items-start">
<div className="h-5 w-5 rounded-full border flex items-center justify-center mb-4 border-gray-500">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="h-2 w-2 text-gray-300"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 4.5l15 15m0 0V8.25m0 11.25H8.25"
/>
</svg>
</div>
<h1 className="font-bold text-xl text-white mb-4 relative z-50">
Meteors because they're cool
</h1>
<p className="font-normal text-base text-slate-500 mb-4 relative z-50">
I don't know what to write so I'll just paste something cool
here. One more sentence because lorem ipsum is boring.
</p>
<button className="border px-4 py-1 rounded-lg border-gray-500 text-gray-300">
Explore
</button>
<Meteors number={20} />
</div>
</div>
)
}Aurora Background
An animated aurora borealis background - a northern lights effect.
// components/ui/aurora-background.tsx
'use client'
import { cn } from '@/lib/utils'
import React, { ReactNode } from 'react'
interface AuroraBackgroundProps extends React.HTMLProps<HTMLDivElement> {
children: ReactNode
showRadialGradient?: boolean
}
export const AuroraBackground = ({
className,
children,
showRadialGradient = true,
...props
}: AuroraBackgroundProps) => {
return (
<main>
<div
className={cn(
'relative flex flex-col h-[100vh] items-center justify-center bg-zinc-50 dark:bg-zinc-900 text-slate-950 transition-bg',
className
)}
{...props}
>
<div className="absolute inset-0 overflow-hidden">
<div
className={cn(
`
[--white-gradient:repeating-linear-gradient(100deg,var(--white)_0%,var(--white)_7%,var(--transparent)_10%,var(--transparent)_12%,var(--white)_16%)]
[--dark-gradient:repeating-linear-gradient(100deg,var(--black)_0%,var(--black)_7%,var(--transparent)_10%,var(--transparent)_12%,var(--black)_16%)]
[--aurora:repeating-linear-gradient(100deg,var(--blue-500)_10%,var(--indigo-300)_15%,var(--blue-300)_20%,var(--violet-200)_25%,var(--blue-400)_30%)]
[background-image:var(--white-gradient),var(--aurora)]
dark:[background-image:var(--dark-gradient),var(--aurora)]
[background-size:300%,_200%]
[background-position:50%_50%,50%_50%]
filter blur-[10px] invert dark:invert-0
after:content-[""] after:absolute after:inset-0 after:[background-image:var(--white-gradient),var(--aurora)]
after:dark:[background-image:var(--dark-gradient),var(--aurora)]
after:[background-size:200%,_100%]
after:animate-aurora after:[background-attachment:fixed] after:mix-blend-difference
pointer-events-none
absolute -inset-[10px] opacity-50 will-change-transform`,
showRadialGradient &&
`[mask-image:radial-gradient(ellipse_at_100%_0%,black_10%,var(--transparent)_70%)]`
)}
></div>
</div>
{children}
</div>
</main>
)
}Aurora Background usage example:
'use client'
import { motion } from 'framer-motion'
import React from 'react'
import { AuroraBackground } from '@/components/ui/aurora-background'
export function AuroraBackgroundDemo() {
return (
<AuroraBackground>
<motion.div
initial={{ opacity: 0.0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{
delay: 0.3,
duration: 0.8,
ease: 'easeInOut',
}}
className="relative flex flex-col gap-4 items-center justify-center px-4"
>
<div className="text-3xl md:text-7xl font-bold dark:text-white text-center">
Background lights are cool you know.
</div>
<div className="font-extralight text-base md:text-4xl dark:text-neutral-200 py-4">
And this, is chemical burn.
</div>
<button className="bg-black dark:bg-white rounded-full w-fit text-white dark:text-black px-4 py-2">
Debug now
</button>
</motion.div>
</AuroraBackground>
)
}Sparkles Effect
Animated sparkles around an element - a subtle magical effect.
// components/ui/sparkles.tsx
'use client'
import React, { useId, useMemo } from 'react'
import { motion } from 'framer-motion'
import { cn } from '@/lib/utils'
interface SparkleProps {
id: string
createdAt: number
color: string
size: number
style: {
top: string
left: string
zIndex: number
}
}
interface SparklesProps {
className?: string
children: React.ReactNode
sparklesCount?: number
colors?: {
first: string
second: string
}
}
const DEFAULT_SPARKLE_COLORS = {
first: '#9E7AFF',
second: '#FE8BBB',
}
const generateSparkle = (colors = DEFAULT_SPARKLE_COLORS): SparkleProps => {
return {
id: String(Math.random()),
createdAt: Date.now(),
color: Math.random() > 0.5 ? colors.first : colors.second,
size: Math.random() * 10 + 10,
style: {
top: Math.random() * 100 + '%',
left: Math.random() * 100 + '%',
zIndex: 2,
},
}
}
const Sparkle = ({ color, size, style }: Omit<SparkleProps, 'id' | 'createdAt'>) => {
return (
<motion.svg
width={size}
height={size}
viewBox="0 0 160 160"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={style}
className="absolute pointer-events-none"
initial={{ scale: 0, rotate: 0 }}
animate={{
scale: [0, 1, 0],
rotate: [0, 180],
}}
transition={{
duration: 1.5,
repeat: Infinity,
repeatDelay: Math.random() * 2,
}}
>
<path
d="M80 0C80 0 84.2846 41.2925 101.496 58.504C118.707 75.7154 160 80 160 80C160 80 118.707 84.2846 101.496 101.496C84.2846 118.707 80 160 80 160C80 160 75.7154 118.707 58.504 101.496C41.2925 84.2846 0 80 0 80C0 80 41.2925 75.7154 58.504 58.504C75.7154 41.2925 80 0 80 0Z"
fill={color}
/>
</motion.svg>
)
}
export const Sparkles: React.FC<SparklesProps> = ({
children,
className,
sparklesCount = 10,
colors = DEFAULT_SPARKLE_COLORS,
}) => {
const sparkles = useMemo(() => {
return Array.from({ length: sparklesCount }, () => generateSparkle(colors))
}, [sparklesCount, colors])
return (
<span className={cn('relative inline-block', className)}>
{sparkles.map((sparkle) => (
<Sparkle
key={sparkle.id}
color={sparkle.color}
size={sparkle.size}
style={sparkle.style}
/>
))}
<span className="relative z-10">{children}</span>
</span>
)
}Sparkles usage example:
import { Sparkles } from '@/components/ui/sparkles'
export function SparklesPreview() {
return (
<div className="h-40 w-full bg-black flex items-center justify-center">
<Sparkles
sparklesCount={12}
colors={{
first: '#A07CFE',
second: '#FE8FB5',
}}
>
<span className="text-4xl font-bold text-white">
Magical Text
</span>
</Sparkles>
</div>
)
}Lamp Effect
A glowing lamp with animated light - a dramatic hero effect.
// components/ui/lamp.tsx
'use client'
import React from 'react'
import { motion } from 'framer-motion'
import { cn } from '@/lib/utils'
export const LampContainer = ({
children,
className,
}: {
children: React.ReactNode
className?: string
}) => {
return (
<div
className={cn(
'relative flex min-h-screen flex-col items-center justify-center overflow-hidden bg-slate-950 w-full rounded-md z-0',
className
)}
>
<div className="relative flex w-full flex-1 scale-y-125 items-center justify-center isolate z-0">
<motion.div
initial={{ opacity: 0.5, width: '15rem' }}
whileInView={{ opacity: 1, width: '30rem' }}
transition={{
delay: 0.3,
duration: 0.8,
ease: 'easeInOut',
}}
style={{
backgroundImage: `conic-gradient(var(--conic-position), var(--tw-gradient-stops))`,
}}
className="absolute inset-auto right-1/2 h-56 overflow-visible w-[30rem] bg-gradient-conic from-cyan-500 via-transparent to-transparent text-white [--conic-position:from_70deg_at_center_top]"
>
<div className="absolute w-[100%] left-0 bg-slate-950 h-40 bottom-0 z-20 [mask-image:linear-gradient(to_top,white,transparent)]" />
<div className="absolute w-40 h-[100%] left-0 bg-slate-950 bottom-0 z-20 [mask-image:linear-gradient(to_right,white,transparent)]" />
</motion.div>
<motion.div
initial={{ opacity: 0.5, width: '15rem' }}
whileInView={{ opacity: 1, width: '30rem' }}
transition={{
delay: 0.3,
duration: 0.8,
ease: 'easeInOut',
}}
style={{
backgroundImage: `conic-gradient(var(--conic-position), var(--tw-gradient-stops))`,
}}
className="absolute inset-auto left-1/2 h-56 w-[30rem] bg-gradient-conic from-transparent via-transparent to-cyan-500 text-white [--conic-position:from_290deg_at_center_top]"
>
<div className="absolute w-40 h-[100%] right-0 bg-slate-950 bottom-0 z-20 [mask-image:linear-gradient(to_left,white,transparent)]" />
<div className="absolute w-[100%] right-0 bg-slate-950 h-40 bottom-0 z-20 [mask-image:linear-gradient(to_top,white,transparent)]" />
</motion.div>
<div className="absolute top-1/2 h-48 w-full translate-y-12 scale-x-150 bg-slate-950 blur-2xl"></div>
<div className="absolute top-1/2 z-50 h-48 w-full bg-transparent opacity-10 backdrop-blur-md"></div>
<div className="absolute inset-auto z-50 h-36 w-[28rem] -translate-y-1/2 rounded-full bg-cyan-500 opacity-50 blur-3xl"></div>
<motion.div
initial={{ width: '8rem' }}
whileInView={{ width: '16rem' }}
transition={{
delay: 0.3,
duration: 0.8,
ease: 'easeInOut',
}}
className="absolute inset-auto z-30 h-36 w-64 -translate-y-[6rem] rounded-full bg-cyan-400 blur-2xl"
></motion.div>
<motion.div
initial={{ width: '15rem' }}
whileInView={{ width: '30rem' }}
transition={{
delay: 0.3,
duration: 0.8,
ease: 'easeInOut',
}}
className="absolute inset-auto z-50 h-0.5 w-[30rem] -translate-y-[7rem] bg-cyan-400"
></motion.div>
<div className="absolute inset-auto z-40 h-44 w-full -translate-y-[12.5rem] bg-slate-950"></div>
</div>
<div className="relative z-50 flex -translate-y-80 flex-col items-center px-5">
{children}
</div>
</div>
)
}Lamp Effect usage example:
'use client'
import React from 'react'
import { motion } from 'framer-motion'
import { LampContainer } from '@/components/ui/lamp'
export function LampDemo() {
return (
<LampContainer>
<motion.h1
initial={{ opacity: 0.5, y: 100 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{
delay: 0.3,
duration: 0.8,
ease: 'easeInOut',
}}
className="mt-8 bg-gradient-to-br from-slate-300 to-slate-500 py-4 bg-clip-text text-center text-4xl font-medium tracking-tight text-transparent md:text-7xl"
>
Build lamps <br /> the right way
</motion.h1>
</LampContainer>
)
}Background Beams
Animated light beams in the background.
// components/ui/background-beams.tsx
'use client'
import React from 'react'
import { motion } from 'framer-motion'
import { cn } from '@/lib/utils'
export const BackgroundBeams = ({ className }: { className?: string }) => {
const paths = [
'M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875',
'M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867',
'M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859',
'M-359 -213C-359 -213 -291 192 173 319C637 446 705 851 705 851',
'M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843',
'M-345 -229C-345 -229 -277 176 187 303C651 430 719 835 719 835',
'M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827',
'M-331 -245C-331 -245 -263 160 201 287C665 414 733 819 733 819',
'M-324 -253C-324 -253 -256 152 208 279C672 406 740 811 740 811',
'M-317 -261C-317 -261 -249 144 215 271C679 398 747 803 747 803',
]
return (
<div
className={cn(
'absolute h-full w-full inset-0 [mask-size:40px] [mask-repeat:no-repeat] flex items-center justify-center',
className
)}
>
<svg
className="z-0 h-full w-full pointer-events-none absolute"
width="100%"
height="100%"
viewBox="0 0 696 316"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{paths.map((path, index) => (
<motion.path
key={`path-` + index}
d={path}
stroke={`url(#linearGradient-${index})`}
strokeOpacity="0.4"
strokeWidth="0.5"
></motion.path>
))}
<defs>
{paths.map((_, index) => (
<motion.linearGradient
id={`linearGradient-${index}`}
key={`gradient-${index}`}
initial={{
x1: '0%',
x2: '0%',
y1: '0%',
y2: '0%',
}}
animate={{
x1: ['0%', '100%'],
x2: ['0%', '95%'],
y1: ['0%', '100%'],
y2: ['0%', `${93 + Math.random() * 8}%`],
}}
transition={{
duration: Math.random() * 10 + 10,
ease: 'easeInOut',
repeat: Infinity,
delay: Math.random() * 10,
}}
>
<stop stopColor="#18CCFC" stopOpacity="0"></stop>
<stop stopColor="#18CCFC"></stop>
<stop offset="32.5%" stopColor="#6344F5"></stop>
<stop offset="100%" stopColor="#AE48FF" stopOpacity="0"></stop>
</motion.linearGradient>
))}
</defs>
</svg>
</div>
)
}Text Generate Effect
Animated text generation letter by letter.
// components/ui/text-generate-effect.tsx
'use client'
import { useEffect } from 'react'
import { motion, stagger, useAnimate } from 'framer-motion'
import { cn } from '@/lib/utils'
export const TextGenerateEffect = ({
words,
className,
filter = true,
duration = 0.5,
}: {
words: string
className?: string
filter?: boolean
duration?: number
}) => {
const [scope, animate] = useAnimate()
const wordsArray = words.split(' ')
useEffect(() => {
animate(
'span',
{
opacity: 1,
filter: filter ? 'blur(0px)' : 'none',
},
{
duration: duration ? duration : 1,
delay: stagger(0.2),
}
)
}, [scope.current])
const renderWords = () => {
return (
<motion.div ref={scope}>
{wordsArray.map((word, idx) => {
return (
<motion.span
key={word + idx}
className="dark:text-white text-black opacity-0"
style={{
filter: filter ? 'blur(10px)' : 'none',
}}
>
{word}{' '}
</motion.span>
)
})}
</motion.div>
)
}
return (
<div className={cn('font-bold', className)}>
<div className="mt-4">
<div className="dark:text-white text-black text-2xl leading-snug tracking-wide">
{renderWords()}
</div>
</div>
</div>
)
}Text Generate Effect usage example:
import { TextGenerateEffect } from '@/components/ui/text-generate-effect'
const words = `Oxygen gets you high. In a catastrophic emergency, we're taking giant, panicked breaths. Suddenly you become euphoric, docile. You accept your fate.`
export function TextGenerateEffectDemo() {
return <TextGenerateEffect words={words} />
}Advanced components
Animated Tabs
// components/ui/animated-tabs.tsx
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
import { cn } from '@/lib/utils'
type Tab = {
title: string
value: string
content?: string | React.ReactNode
}
export const Tabs = ({
tabs: propTabs,
containerClassName,
activeTabClassName,
tabClassName,
contentClassName,
}: {
tabs: Tab[]
containerClassName?: string
activeTabClassName?: string
tabClassName?: string
contentClassName?: string
}) => {
const [active, setActive] = useState<Tab>(propTabs[0])
const [tabs, setTabs] = useState<Tab[]>(propTabs)
const moveSelectedTabToTop = (idx: number) => {
const newTabs = [...propTabs]
const selectedTab = newTabs.splice(idx, 1)
newTabs.unshift(selectedTab[0])
setTabs(newTabs)
setActive(newTabs[0])
}
const [hovering, setHovering] = useState(false)
return (
<>
<div
className={cn(
'flex flex-row items-center justify-start [perspective:1000px] relative overflow-auto sm:overflow-visible no-visible-scrollbar max-w-full w-full',
containerClassName
)}
>
{propTabs.map((tab, idx) => (
<button
key={tab.title}
onClick={() => {
moveSelectedTabToTop(idx)
}}
onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
className={cn('relative px-4 py-2 rounded-full', tabClassName)}
style={{
transformStyle: 'preserve-3d',
}}
>
{active.value === tab.value && (
<motion.div
layoutId="clickedbutton"
transition={{ type: 'spring', bounce: 0.3, duration: 0.6 }}
className={cn(
'absolute inset-0 bg-gray-200 dark:bg-zinc-800 rounded-full',
activeTabClassName
)}
/>
)}
<span className="relative block text-black dark:text-white">
{tab.title}
</span>
</button>
))}
</div>
<FadeInDiv
tabs={tabs}
active={active}
key={active.value}
hovering={hovering}
className={cn('mt-32', contentClassName)}
/>
</>
)
}
export const FadeInDiv = ({
className,
tabs,
hovering,
}: {
className?: string
key?: string
tabs: Tab[]
active: Tab
hovering?: boolean
}) => {
const isActive = (tab: Tab) => {
return tab.value === tabs[0].value
}
return (
<div className="relative w-full h-full">
{tabs.map((tab, idx) => (
<motion.div
key={tab.value}
layoutId={tab.value}
style={{
scale: 1 - idx * 0.1,
top: hovering ? idx * -50 : 0,
zIndex: -idx,
opacity: idx < 3 ? 1 - idx * 0.1 : 0,
}}
animate={{
y: isActive(tab) ? [0, 40, 0] : 0,
}}
className={cn('w-full h-full absolute top-0 left-0', className)}
>
{tab.content}
</motion.div>
))}
</div>
)
}Infinite Moving Cards
Infinitely scrolling cards - perfect for testimonials.
// components/ui/infinite-moving-cards.tsx
'use client'
import { cn } from '@/lib/utils'
import React, { useEffect, useState } from 'react'
export const InfiniteMovingCards = ({
items,
direction = 'left',
speed = 'fast',
pauseOnHover = true,
className,
}: {
items: {
quote: string
name: string
title: string
}[]
direction?: 'left' | 'right'
speed?: 'fast' | 'normal' | 'slow'
pauseOnHover?: boolean
className?: string
}) => {
const containerRef = React.useRef<HTMLDivElement>(null)
const scrollerRef = React.useRef<HTMLUListElement>(null)
useEffect(() => {
addAnimation()
}, [])
const [start, setStart] = useState(false)
function addAnimation() {
if (containerRef.current && scrollerRef.current) {
const scrollerContent = Array.from(scrollerRef.current.children)
scrollerContent.forEach((item) => {
const duplicatedItem = item.cloneNode(true)
if (scrollerRef.current) {
scrollerRef.current.appendChild(duplicatedItem)
}
})
getDirection()
getSpeed()
setStart(true)
}
}
const getDirection = () => {
if (containerRef.current) {
if (direction === 'left') {
containerRef.current.style.setProperty(
'--animation-direction',
'forwards'
)
} else {
containerRef.current.style.setProperty(
'--animation-direction',
'reverse'
)
}
}
}
const getSpeed = () => {
if (containerRef.current) {
if (speed === 'fast') {
containerRef.current.style.setProperty('--animation-duration', '20s')
} else if (speed === 'normal') {
containerRef.current.style.setProperty('--animation-duration', '40s')
} else {
containerRef.current.style.setProperty('--animation-duration', '80s')
}
}
}
return (
<div
ref={containerRef}
className={cn(
'scroller relative z-20 max-w-7xl overflow-hidden [mask-image:linear-gradient(to_right,transparent,white_20%,white_80%,transparent)]',
className
)}
>
<ul
ref={scrollerRef}
className={cn(
'flex min-w-full shrink-0 gap-4 py-4 w-max flex-nowrap',
start && 'animate-scroll',
pauseOnHover && 'hover:[animation-play-state:paused]'
)}
>
{items.map((item, idx) => (
<li
className="w-[350px] max-w-full relative rounded-2xl border border-b-0 flex-shrink-0 border-slate-700 px-8 py-6 md:w-[450px]"
style={{
background:
'linear-gradient(180deg, var(--slate-800), var(--slate-900)',
}}
key={item.name + idx}
>
<blockquote>
<div
aria-hidden="true"
className="user-select-none -z-1 pointer-events-none absolute -left-0.5 -top-0.5 h-[calc(100%_+_4px)] w-[calc(100%_+_4px)]"
></div>
<span className="relative z-20 text-sm leading-[1.6] text-gray-100 font-normal">
{item.quote}
</span>
<div className="relative z-20 mt-6 flex flex-row items-center">
<span className="flex flex-col gap-1">
<span className="text-sm leading-[1.6] text-gray-400 font-normal">
{item.name}
</span>
<span className="text-sm leading-[1.6] text-gray-400 font-normal">
{item.title}
</span>
</span>
</div>
</blockquote>
</li>
))}
</ul>
</div>
)
}Building a complete page with Aceternity UI
Landing page example
// app/page.tsx
import { Spotlight } from '@/components/ui/spotlight'
import { TextGenerateEffect } from '@/components/ui/text-generate-effect'
import { CardContainer, CardBody, CardItem } from '@/components/ui/3d-card'
import { InfiniteMovingCards } from '@/components/ui/infinite-moving-cards'
import { Meteors } from '@/components/ui/meteors'
const testimonials = [
{
quote: "This product changed the way we work. Absolutely amazing experience!",
name: "Sarah Johnson",
title: "CEO at TechCorp",
},
{
quote: "Best investment we've made this year. The results speak for themselves.",
name: "Michael Chen",
title: "CTO at StartupXYZ",
},
{
quote: "Incredible customer support and outstanding product quality.",
name: "Emily Williams",
title: "Product Manager at InnovateCo",
},
]
const features = [
{
title: "Lightning Fast",
description: "Optimized for speed with cutting-edge technology",
icon: "⚡",
},
{
title: "Secure by Default",
description: "Enterprise-grade security built into every feature",
icon: "🔒",
},
{
title: "AI Powered",
description: "Smart automation that learns and adapts to your needs",
icon: "🤖",
},
]
export default function LandingPage() {
return (
<main className="min-h-screen bg-black/[0.96] antialiased bg-grid-white/[0.02]">
{/* Hero Section with Spotlight */}
<section className="h-screen w-full flex md:items-center md:justify-center relative overflow-hidden">
<Spotlight
className="-top-40 left-0 md:left-60 md:-top-20"
fill="white"
/>
<div className="p-4 max-w-7xl mx-auto relative z-10 w-full pt-20 md:pt-0">
<h1 className="text-4xl md:text-7xl font-bold text-center bg-clip-text text-transparent bg-gradient-to-b from-neutral-50 to-neutral-400">
Build Amazing
<br />
Products Faster
</h1>
<TextGenerateEffect
words="Transform your ideas into reality with our cutting-edge platform.
Start building today and see results tomorrow."
className="mt-8 text-center max-w-2xl mx-auto"
/>
<div className="flex gap-4 justify-center mt-10">
<button className="px-8 py-3 rounded-full bg-white text-black font-semibold hover:bg-gray-200 transition">
Get Started
</button>
<button className="px-8 py-3 rounded-full border border-white/20 text-white hover:bg-white/10 transition">
Learn More
</button>
</div>
</div>
</section>
{/* Features Section with 3D Cards */}
<section className="py-20 px-4">
<h2 className="text-3xl md:text-5xl font-bold text-center text-white mb-16">
Powerful Features
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-6xl mx-auto">
{features.map((feature, idx) => (
<CardContainer key={idx} className="inter-var">
<CardBody className="bg-gray-900 relative group/card border-white/[0.1] w-full h-auto rounded-xl p-6 border">
<CardItem
translateZ="50"
className="text-4xl mb-4"
>
{feature.icon}
</CardItem>
<CardItem
translateZ="60"
className="text-xl font-bold text-white"
>
{feature.title}
</CardItem>
<CardItem
as="p"
translateZ="80"
className="text-neutral-400 text-sm mt-2"
>
{feature.description}
</CardItem>
</CardBody>
</CardContainer>
))}
</div>
</section>
{/* Testimonials Section */}
<section className="py-20 relative">
<h2 className="text-3xl md:text-5xl font-bold text-center text-white mb-16">
What Our Customers Say
</h2>
<InfiniteMovingCards
items={testimonials}
direction="right"
speed="slow"
/>
</section>
{/* CTA Section with Meteors */}
<section className="py-20 px-4 relative overflow-hidden">
<div className="max-w-2xl mx-auto relative">
<div className="absolute inset-0 h-full w-full bg-gradient-to-r from-blue-500 to-teal-500 transform scale-[0.80] rounded-full blur-3xl" />
<div className="relative shadow-xl bg-gray-900 border border-gray-800 px-8 py-12 rounded-2xl overflow-hidden">
<h2 className="text-3xl font-bold text-white text-center mb-4">
Ready to Get Started?
</h2>
<p className="text-neutral-400 text-center mb-8">
Join thousands of satisfied customers today.
</p>
<div className="flex justify-center">
<button className="px-8 py-3 rounded-full bg-gradient-to-r from-blue-500 to-teal-500 text-white font-semibold hover:opacity-90 transition">
Start Free Trial
</button>
</div>
<Meteors number={20} />
</div>
</div>
</section>
</main>
)
}Best practices
1. Performance optimization
import dynamic from 'next/dynamic'
const Spotlight = dynamic(
() => import('@/components/ui/spotlight').then((mod) => mod.Spotlight),
{ ssr: false }
)
const Meteors = dynamic(
() => import('@/components/ui/meteors').then((mod) => mod.Meteors),
{ ssr: false }
)2. Responsiveness
export function ResponsiveSpotlight() {
const isMobile = useMediaQuery('(max-width: 768px)')
return (
<div className="relative">
{!isMobile && (
<Spotlight className="-top-40 left-0 md:left-60 md:-top-20" fill="white" />
)}
<div className="relative z-10">
{/* Content */}
</div>
</div>
)
}3. Accessibility
const prefersReducedMotion = usePrefersReducedMotion()
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: prefersReducedMotion ? 0 : 0.8,
}}
>
{children}
</motion.div>FAQ - frequently asked questions
Is Aceternity UI free?
Yes, Aceternity UI is completely free and open-source. All components can be copied and used in commercial projects without any fees.
What are the requirements for using Aceternity UI?
You need a React project with Tailwind CSS and Framer Motion. The components are designed for Next.js, but they also work with Create React App and Vite.
Do the components work with Server Components?
Most Aceternity UI components require 'use client' due to Framer Motion animations. You can import them into Server Components as a Client Component wrapper.
How do I customize colors and styles?
The components use Tailwind CSS, so you can easily modify classes directly in the code. Colors are often defined as CSS variables or gradients.
Can I use Aceternity UI with TypeScript?
Yes, all components have full TypeScript support with prop types.
How do I troubleshoot animation issues?
Make sure Framer Motion is installed and that the component has the 'use client' directive. Also check that your Tailwind config contains the necessary keyframes.
Are the components responsive?
Yes, the components are designed mobile-first with Tailwind breakpoints. Some 3D effects may be disabled on mobile for performance reasons.
Pricing
| Plan | Price | Contents |
|---|---|---|
| Open Source | Free | All components, copy-paste |
| Pro Templates | $49-199 | Ready-made landing page templates |
| Custom Development | On request | Dedicated components |
Summary
Aceternity UI is a powerful collection of React components with 3D effects and animations:
- 50+ components with visual effects
- Framer Motion for smooth 60fps animations
- Tailwind CSS for easy customization
- TypeScript with full type support
- Copy-paste approach with no additional dependencies
- Dark mode out of the box
Perfect for creating spectacular landing pages and marketing websites.