Aceternity UI, React components with 3D effects
Aceternity UI is a set of React components you copy straight into your project rather than install as a dependency. The specialisation is narrow and clearly stated: visual effects, meaning cards that tilt in three dimensions under the mouse, animated backgrounds, spotlights that track the cursor, and testimonial carousels that scroll forever. The technical base is Tailwind CSS and Motion, the animation library previously known as Framer Motion.
How Aceternity UI differs from an ordinary component library
The distinction to start with concerns the distribution model. A classic library is a package in node_modules that you update with a command and whose source you rarely read. Aceternity UI works the other way round: you copy a component file into your repository, and from that moment it is your code, with your commits and your responsibility for maintaining it.
That model carries a consequence worth understanding before you use it. Updates do not arrive on their own. If the author fixes a bug in the 3D card six months after you copied it, your copy keeps the bug until you go and look at the repository yourself. In exchange you get complete freedom: you can rewrite half the component and nothing breaks the next time dependencies install.
The second distinction concerns purpose. This is not a library for building applications. You will not find an accessible checkbox, a properly handled modal dialog, or a sortable table here. The component set is chosen for marketing pages: opening sections, feature showcases, customer testimonials, calls to action. If you are building an admin panel you want shadcn/ui or Radix UI, with Aceternity at most on that panel's sales page.
The third is the scale of what these animations attempt. The effects here are deliberately loud. A full screen aurora, a spotlight brightening half a section, meteors streaking across a card. That works on a product page that wants to look current, and looks wrong on a law firm's site. Choosing this library is an aesthetic decision before it is a technical one.
Aceternity UI against the alternatives
| Feature | Aceternity UI | Magic UI | shadcn/ui | React Bits |
|---|---|---|---|---|
| Primary area | 3D effects and backgrounds | micro animations and sections | application components | text and background effects |
| Distribution model | copy the code | copy the code | copy the code | copy the code |
| Motion dependency | required | required | optional | partial |
| Accessibility and keyboard support | out of scope | out of scope | a priority | out of scope |
| Paid tier | yes, Pro templates | yes, Pro templates | no | no |
| Typical use | sales page | sales page | product interface | sales page |
A practical note on that table: these libraries do not exclude each other and usually coexist in a real project. Since each one is copied Tailwind code, nothing stops the hero section coming from Aceternity, the contact form from shadcn, and the stats counter from Magic UI. The only thing to watch is consistency in the cn helper and theme variables, because each library assumes a slightly different setup.
Installation and configuration
Required dependencies
The library's documentation now targets Next.js 15 and Tailwind CSS version 4, with animation handled by the motion package. That is the same library we knew for years as framer-motion, after the 2024 rename. Both names still publish to the npm registry at the same version, 13.1.0 at the time of writing, so older projects importing framer-motion keep working and there is no urgent reason to rewrite them.
npm install motion clsx tailwind-merge
npm install @tabler/icons-react
npm install simplex-noiseThe simplex-noise package is only useful in components generating procedural noise, wavy backgrounds for instance. Do not install it speculatively, since it adds tens of kilobytes to the bundle that you will never use.
One thing is worth settling immediately, because it saves confusion around the first component. Aceternity publishes no npm package of components. You add them by copying code from the site or by running npx shadcn@latest add @aceternity/component-name, which drops the file into a directory you name. Either way the result is a file in your repository, not an entry in package.json.
Tailwind CSS configuration
The configuration below describes Tailwind version 3, the layout with a tailwind.config.js file. On version 4 the same content moves into a stylesheet: keyframes go into plain @keyframes and animations register through @theme instead of extending a config object. I am leaving the config file variant here because most existing projects still run on it, and moving these declarations into CSS is a ten minute job.
// 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>The performance cost the demos never show
The demo site runs beautifully for its author, because it presents one component at a time on a strong laptop. A real sales page stacks several of these effects one under another, and that is when questions arise that no demo will ask.
The first problem is bundle cost. The animation package itself is a few tens of kilobytes compressed, but the real overhead grows with every component that brings its own code. An aurora, meteors, sparkles, and a 3D card together can add more to first load than the rest of the page. Nobody notices that on fibre, and plenty of people notice it on a phone with weak reception.
The second is painting. Effects built on large blurs, meaning the aurora, the glows, and the lamp, force the browser to recompute sizeable areas of the screen on every frame. A blur filter with a radius of several dozen pixels stretched across a viewport is one of the most expensive things you can ask a browser to do in CSS. On a machine with a discrete GPU it is invisible; on a mid range Android phone you see the scroll stutter.
The third is components doing work on every mouse move. The 3D card recomputes cursor position and writes a transform straight onto the element. As long as there are three such cards everything is fine. At a grid of twelve in view it starts to show, particularly since each instance holds its own state and its own context.
The practical conclusions are three and all reduce to restraint. One strong effect per screen is enough; two compete for attention. Heavy backgrounds are worth disabling on mobile rather than merely scaling down. Components below the fold should load dynamically so they do not block first render.
The naming deserves a separate note. What this library calls a 3D effect is a CSS transform: the element tilts after the cursor while remaining a flat rectangle. A real scene rendered by the GPU is a different category of tool, Spline for instance, and a different order of cost: several megabytes to download and continuous render looping for as long as the scene stays visible. If what you want is a sense of depth on a landing page, the CSS tilt is usually the right choice, since it costs a fraction of that.
Accessibility, this library's largest gap
This needs stating plainly, because the documentation does not. Aceternity UI does not address accessibility and does not pretend to. The components carry no role attributes, no keyboard handling, and no focus management. The animated tab strip switches on click and will not answer arrow keys. That is not a bug in the library but a consequence of its purpose: this is a decorative layer, not an interaction layer.
The concrete obligation that lands on you concerns reduced motion preferences. Some users have told their system they do not want animation, usually because of vertigo or migraine. The browser exposes that through a media query, and respecting it is your job.
@media (prefers-reduced-motion: reduce) {
.animate-aurora,
.animate-meteor-effect,
.animate-spotlight {
animation: none;
}
}Four lines in a global stylesheet settle the matter for keyframe based effects. Components driven from JavaScript need a hook reading the same preference and zeroing animation duration, which the best practices section above demonstrates.
The second obligation concerns contrast. Text over an animated background can be legible in one phase of the animation and illegible in another. Check contrast at the worst moment of the cycle, not on a screenshot taken at a random instant.
Licensing and the paid tier
The free part of the library covers the components published on the site, and you may use them in commercial projects at no cost. Because you copy the code into your own repository, there is no licence key mechanism and no build time verification, unlike some paid libraries where a missing key blocks a production run.
Ready made Pro templates and blocks are sold separately, meaning complete composed pages rather than individual components. Alongside the free tier the pricing page lists three paid options: annual access at 169 dollars, permanent access for a one off 199 dollars, and a team option covering ten people for a one off 1590 dollars. Each of those amounts is discounted at the time of writing against 249, 299, and 1990 dollars respectively, and the vendor gives no end date for the discount, so check the rates directly before buying. The scope is the same in every option, over two hundred paid blocks and a dozen or so templates, and what differs is the update window: one year on the annual option, future releases with no time limit on the permanent one. The vendor offers no refunds, since a purchase exposes the entire code immediately.
It is worth knowing what the money actually buys, because that question recurs. What you pay for is composition, not technology. The free components hand you building blocks; the paid templates hand you a page assembled from them, including the decisions about spacing, typography, and section order. If you have a designer or a good sense of layout, the paid tier adds little. If you are assembling the page alone and can see it turning into a pile of effects with no rhythm, that is exactly where the value sits.
Common mistakes
The most frequent involves a missing 'use client' directive. These components use state, refs, and animations that run in the browser, so in Next.js they must be client components. The error surfaces as a complaint about a hook in a server component and often gets blamed on the library.
The second is copying a component without adding its keyframes to the Tailwind config. The code compiles, the component renders, the animation simply never starts, because the animate-aurora class does not exist. Always move both parts together: the component file and the entries from the keyframes section.
The third is class conflicts when you add your own styles. The cn helper exists precisely to merge Tailwind classes with correct precedence, and skipping it means your p-8 loses to a built in p-6 in a way that depends on stylesheet order. There is more on that mechanic in the Tailwind CSS writeup.
The fourth is randomness computed during render. The meteors component derives positions from a random number generator, so the server and the browser produce different results and React reports a tree mismatch. The fix is to compute positions in an effect after mount, or to disable server rendering for that component.
The fifth is treating these components as a stable interface. You copied the code, so you hold its state as of a particular day. When React or Motion updates, verifying that nothing broke is on you, and nobody will notify you.
FAQ
Is Aceternity UI free?
The entire component library visible on the site is free and usable commercially at no cost. What is paid is a separate tier of ready made Pro templates and blocks, meaning composed pages rather than individual components.
Do the components work with React Server Components?
Not directly. Nearly every component in this library needs a 'use client' directive, because it relies on state, element refs, or mouse events. You can import them inside server components, but the component file itself must be a client one.
Do I have to migrate framer-motion to motion?
There is no need. Both package names publish in parallel at the same version, so existing imports keep working. Use the motion name in new projects, since that is the current name and the one the documentation follows.
Are these components accessible to keyboard users?
No, the library does not address accessibility. There is no keyboard handling, no focus management, and no role attributes. For an interface where that matters, use shadcn/ui or Radix UI and leave Aceternity to the decorative layer.
Can Aceternity UI be used outside Next.js?
Yes, the components are ordinary React with Tailwind, so they work in a Vite project or any other setup. The only thing that drops out is examples using the Next.js image component, where a plain tag substitutes fine.
The full component list lives in the Aceternity UI documentation, and animation details are in the Motion for React documentation.