Magic UI - Kompletny Przewodnik po Animowanych Komponentach React
Czym jest Magic UI?
Magic UI to kolekcja pięknych, gotowych do użycia animowanych komponentów React, które możesz skopiować i wkleić do swojego projektu. Zbudowane na fundamencie shadcn/ui i Framer Motion, komponenty Magic UI dodają "magii" do landing pages, portfolio i aplikacji SaaS bez konieczności pisania skomplikowanych animacji od zera.
W przeciwieństwie do tradycyjnych bibliotek komponentów, Magic UI nie jest pakietem npm do instalacji. Zamiast tego, każdy komponent jest dostępny jako kod źródłowy, który kopiujesz do swojego projektu. Ta filozofia "copy-paste" - zapoczątkowana przez shadcn/ui - daje pełną kontrolę nad kodem i możliwość dostosowania do własnych potrzeb.
Magic UI oferuje komponenty, których nie znajdziesz w standardowych bibliotekach UI: animowane beamy łączące elementy (jak w diagramach integracji), shimmer buttons jak u Apple, interaktywne particles reagujące na kursor, efekty spotlight, meteory, typing animations i wiele więcej. Idealne do tworzenia wyróżniających się landing pages i prezentacji produktów.
Filozofia i podejście
Magic UI powstało z obserwacji, że najpiękniejsze strony internetowe (jak te od Stripe, Linear, Vercel) używają custom animacji, które są trudne do odtworzenia. Twórcy Magic UI postanowili zebrać te efekty i udostępnić jako gotowe komponenty React.
Kluczowe zasady Magic UI:
- Copy-paste, nie install - pełna kontrola nad kodem
- Framer Motion - profesjonalne animacje bez boilerplate
- Tailwind CSS - spójne stylowanie
- shadcn/ui compatible - integracja z popularnym systemem
- TypeScript - pełne typy dla lepszego DX
Dlaczego Magic UI?
Kluczowe zalety
- Zero zależności - tylko Framer Motion i Tailwind
- Copy-paste - pełna kontrola, zero vendor lock-in
- TypeScript - kompletne typy dla wszystkich komponentów
- Customizable - kod jest Twój, zmieniaj jak chcesz
- Performance - zoptymalizowane animacje
- Responsive - działa na wszystkich urządzeniach
- Dark mode - natywne wsparcie
- Accessible - zgodne z WCAG gdzie możliwe
Magic UI vs Aceternity UI vs Framer Motion
| Cecha | Magic UI | Aceternity UI | Framer Motion |
|---|---|---|---|
| Typ | Komponenty | Komponenty | Biblioteka |
| Instalacja | Copy-paste | Copy-paste | npm install |
| Customize | Pełna kontrola | Pełna kontrola | Programowanie |
| Komponenty | ~50+ | ~40+ | Build your own |
| Learning curve | Niska | Niska | Średnia |
| TypeScript | ✅ | ✅ | ✅ |
| Dark mode | ✅ | ✅ | N/A |
| shadcn compatible | ✅ | ✅ | N/A |
| Licencja | MIT | MIT | MIT |
Instalacja i setup
Wymagania
# Twój projekt powinien mieć:
- React 18+
- Tailwind CSS
- Framer Motion
- (opcjonalnie) shadcn/ui
# Instalacja zależności
npm install framer-motion clsx tailwind-mergeKonfiguracja Tailwind
// tailwind.config.js
module.exports = {
darkMode: ["class"],
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
],
theme: {
extend: {
animation: {
// Magic UI animations
"shimmer": "shimmer 2s linear infinite",
"spin-slow": "spin 3s linear infinite",
"pulse-slow": "pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite",
"bounce-slow": "bounce 2s infinite",
"meteor": "meteor 5s linear infinite",
"beam": "beam 2s ease-in-out infinite",
},
keyframes: {
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,
},
},
beam: {
"0%": { transform: "translateY(0)" },
"50%": { transform: "translateY(-10px)" },
"100%": { transform: "translateY(0)" },
},
},
},
},
plugins: [require("tailwindcss-animate")],
}Utility functions
// lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}Dodawanie komponentów
# Metoda 1: shadcn CLI (jeśli masz skonfigurowane)
npx shadcn@latest add "https://magicui.design/r/shimmer-button"
# Metoda 2: Ręczne kopiowanie
# 1. Idź na magicui.design
# 2. Wybierz komponent
# 3. Kliknij "Copy"
# 4. Wklej do components/magicui/[nazwa].tsxKomponenty - Buttons
Shimmer Button
// components/magicui/shimmer-button.tsx
import { cn } from "@/lib/utils"
import { motion } from "framer-motion"
interface ShimmerButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
shimmerColor?: string
shimmerSize?: string
borderRadius?: string
shimmerDuration?: string
background?: string
className?: string
children?: React.ReactNode
}
export function ShimmerButton({
shimmerColor = "#ffffff",
shimmerSize = "0.05em",
shimmerDuration = "3s",
borderRadius = "100px",
background = "rgba(0, 0, 0, 1)",
className,
children,
...props
}: ShimmerButtonProps) {
return (
<button
style={{
"--shimmer-color": shimmerColor,
"--shimmer-size": shimmerSize,
"--shimmer-duration": shimmerDuration,
"--border-radius": borderRadius,
"--background": background,
} as React.CSSProperties}
className={cn(
"group relative z-0 flex cursor-pointer items-center justify-center overflow-hidden whitespace-nowrap border border-white/10 px-6 py-3 text-white [background:var(--background)] [border-radius:var(--border-radius)]",
"transform-gpu transition-transform duration-300 ease-in-out active:translate-y-[1px]",
className
)}
{...props}
>
{/* Shimmer effect */}
<div
className={cn(
"absolute inset-0 overflow-hidden [border-radius:var(--border-radius)]",
"before:absolute before:inset-[-100%] before:h-[300%] before:w-[50%]",
"before:animate-[shimmer_var(--shimmer-duration)_linear_infinite]",
"before:bg-gradient-to-r before:from-transparent before:via-[var(--shimmer-color)] before:to-transparent",
"before:opacity-50"
)}
/>
{/* Content */}
<span className="relative z-10 flex items-center gap-2">
{children}
</span>
{/* Glow effect on hover */}
<div
className={cn(
"absolute inset-0 -z-10 blur-xl transition-opacity duration-500",
"bg-gradient-to-br from-purple-500/30 via-pink-500/30 to-blue-500/30",
"opacity-0 group-hover:opacity-100"
)}
/>
</button>
)
}
// Użycie
export default function Hero() {
return (
<ShimmerButton className="shadow-2xl">
<span className="whitespace-pre-wrap text-center text-sm font-medium leading-none tracking-tight text-white lg:text-lg">
Get Started
</span>
</ShimmerButton>
)
}Pulsating Button
// components/magicui/pulsating-button.tsx
import { cn } from "@/lib/utils"
interface PulsatingButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
pulseColor?: string
duration?: string
className?: string
children?: React.ReactNode
}
export function PulsatingButton({
pulseColor = "#0096ff",
duration = "1.5s",
className,
children,
...props
}: PulsatingButtonProps) {
return (
<button
className={cn(
"relative flex cursor-pointer items-center justify-center rounded-lg bg-blue-500 px-6 py-3 text-center text-white",
className
)}
style={{
"--pulse-color": pulseColor,
"--duration": duration,
} as React.CSSProperties}
{...props}
>
{/* Pulsating ring */}
<div className="absolute -inset-1 rounded-lg bg-[var(--pulse-color)] opacity-75 blur animate-pulse" />
{/* Button content */}
<span className="relative flex items-center gap-2">
{children}
</span>
</button>
)
}
// Użycie
<PulsatingButton pulseColor="#7c3aed">
Subscribe Now
</PulsatingButton>Animated Subscribe Button
// components/magicui/animated-subscribe-button.tsx
import { AnimatePresence, motion } from "framer-motion"
import { useState } from "react"
import { cn } from "@/lib/utils"
import { CheckIcon } from "lucide-react"
interface AnimatedSubscribeButtonProps {
buttonColor?: string
buttonTextColor?: string
subscribeStatus?: boolean
initialText: React.ReactNode
changeText: React.ReactNode
className?: string
}
export function AnimatedSubscribeButton({
buttonColor = "#000000",
buttonTextColor = "#ffffff",
subscribeStatus = false,
initialText,
changeText,
className,
}: AnimatedSubscribeButtonProps) {
const [isSubscribed, setIsSubscribed] = useState(subscribeStatus)
return (
<AnimatePresence mode="wait">
{isSubscribed ? (
<motion.button
key="subscribed"
className={cn(
"relative flex items-center justify-center overflow-hidden rounded-lg p-[10px] outline-none",
className
)}
style={{ backgroundColor: buttonColor, color: buttonTextColor }}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
onClick={() => setIsSubscribed(false)}
>
<motion.span
key="checkmark"
className="flex items-center gap-2"
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.1 }}
>
<CheckIcon className="h-4 w-4" />
{changeText}
</motion.span>
</motion.button>
) : (
<motion.button
key="not-subscribed"
className={cn(
"relative flex cursor-pointer items-center justify-center rounded-lg border-none p-[10px]",
className
)}
style={{ backgroundColor: buttonColor, color: buttonTextColor }}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
onClick={() => setIsSubscribed(true)}
>
<motion.span
key="reaction"
className="flex items-center gap-2"
initial={{ x: 0 }}
exit={{ x: 50, transition: { duration: 0.1 } }}
>
{initialText}
</motion.span>
</motion.button>
)}
</AnimatePresence>
)
}
// Użycie
<AnimatedSubscribeButton
buttonColor="#000000"
buttonTextColor="#ffffff"
subscribeStatus={false}
initialText="Subscribe"
changeText="Subscribed!"
/>Komponenty - Text Animations
Typing Animation
// components/magicui/typing-animation.tsx
"use client"
import { useEffect, useState } from "react"
import { cn } from "@/lib/utils"
interface TypingAnimationProps {
text: string
duration?: number
className?: string
}
export function TypingAnimation({
text,
duration = 100,
className,
}: TypingAnimationProps) {
const [displayedText, setDisplayedText] = useState("")
const [index, setIndex] = useState(0)
useEffect(() => {
const typingEffect = setInterval(() => {
if (index < text.length) {
setDisplayedText((prev) => prev + text.charAt(index))
setIndex((prev) => prev + 1)
} else {
clearInterval(typingEffect)
}
}, duration)
return () => clearInterval(typingEffect)
}, [duration, index, text])
return (
<h1
className={cn(
"font-display text-center text-4xl font-bold leading-[5rem] tracking-[-0.02em] drop-shadow-sm",
className
)}
>
{displayedText}
<span className="animate-pulse">|</span>
</h1>
)
}
// Użycie
<TypingAnimation
text="Welcome to Magic UI"
duration={50}
className="text-6xl text-white"
/>Text Reveal
// components/magicui/text-reveal.tsx
"use client"
import { motion, useScroll, useTransform } from "framer-motion"
import { useRef } from "react"
import { cn } from "@/lib/utils"
interface TextRevealProps {
text: string
className?: string
}
export function TextReveal({ text, className }: TextRevealProps) {
const containerRef = useRef<HTMLDivElement>(null)
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start 0.9", "start 0.25"],
})
const words = text.split(" ")
return (
<div
ref={containerRef}
className={cn(
"relative z-0 mx-auto h-[50vh] max-w-4xl",
className
)}
>
<div className="sticky top-0 flex h-1/2 items-center">
<p className="flex flex-wrap text-2xl font-bold text-black/20 dark:text-white/20 md:text-3xl lg:text-4xl xl:text-5xl">
{words.map((word, i) => {
const start = i / words.length
const end = start + 1 / words.length
return (
<Word key={i} progress={scrollYProgress} range={[start, end]}>
{word}
</Word>
)
})}
</p>
</div>
</div>
)
}
interface WordProps {
children: string
progress: any
range: [number, number]
}
function Word({ children, progress, range }: WordProps) {
const opacity = useTransform(progress, range, [0, 1])
return (
<span className="relative mx-1 lg:mx-2.5">
<span className="absolute opacity-30">{children}</span>
<motion.span style={{ opacity }} className="text-black dark:text-white">
{children}
</motion.span>
</span>
)
}
// Użycie (wymaga scroll container)
<TextReveal
text="Magic UI brings your landing pages to life with beautiful animations"
/>Gradient Text
// components/magicui/gradient-text.tsx
import { cn } from "@/lib/utils"
interface GradientTextProps {
children: React.ReactNode
className?: string
colors?: string[]
animationSpeed?: number
}
export function GradientText({
children,
className,
colors = ["#ffaa40", "#9c40ff", "#ffaa40"],
animationSpeed = 8,
}: GradientTextProps) {
const gradientStyle = {
backgroundImage: `linear-gradient(to right, ${colors.join(", ")})`,
backgroundSize: "300% 100%",
animation: `gradient ${animationSpeed}s ease infinite`,
}
return (
<>
<style>{`
@keyframes gradient {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
`}</style>
<span
className={cn(
"bg-clip-text text-transparent",
className
)}
style={gradientStyle}
>
{children}
</span>
</>
)
}
// Użycie
<h1 className="text-6xl font-bold">
<GradientText>Magic UI</GradientText>
</h1>Word Rotate
// components/magicui/word-rotate.tsx
"use client"
import { AnimatePresence, motion } from "framer-motion"
import { useEffect, useState } from "react"
import { cn } from "@/lib/utils"
interface WordRotateProps {
words: string[]
duration?: number
framerProps?: any
className?: string
}
export function WordRotate({
words,
duration = 2500,
framerProps = {
initial: { opacity: 0, y: -50 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: 50 },
transition: { duration: 0.25, ease: "easeOut" },
},
className,
}: WordRotateProps) {
const [index, setIndex] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setIndex((prevIndex) => (prevIndex + 1) % words.length)
}, duration)
return () => clearInterval(interval)
}, [words, duration])
return (
<div className="overflow-hidden py-2">
<AnimatePresence mode="wait">
<motion.h1
key={words[index]}
className={cn("font-bold", className)}
{...framerProps}
>
{words[index]}
</motion.h1>
</AnimatePresence>
</div>
)
}
// Użycie
<div className="text-4xl">
Build{" "}
<WordRotate
words={["beautiful", "amazing", "stunning", "magical"]}
className="text-blue-500"
/>{" "}
websites
</div>Komponenty - Backgrounds
Particles
// components/magicui/particles.tsx
"use client"
import { useEffect, useRef, useState } from "react"
import { cn } from "@/lib/utils"
interface ParticlesProps {
className?: string
quantity?: number
staticity?: number
ease?: number
refresh?: boolean
color?: string
size?: number
}
export function Particles({
className,
quantity = 50,
staticity = 50,
ease = 50,
refresh = false,
color = "#ffffff",
size = 1,
}: ParticlesProps) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const canvasContainerRef = useRef<HTMLDivElement>(null)
const context = useRef<CanvasRenderingContext2D | null>(null)
const circles = useRef<any[]>([])
const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 })
const canvasSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 })
const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1
useEffect(() => {
if (canvasRef.current) {
context.current = canvasRef.current.getContext("2d")
}
initCanvas()
animate()
window.addEventListener("resize", initCanvas)
return () => {
window.removeEventListener("resize", initCanvas)
}
}, [color])
useEffect(() => {
onMouseMove()
}, [])
const initCanvas = () => {
resizeCanvas()
drawParticles()
}
const onMouseMove = () => {
if (canvasContainerRef.current) {
canvasContainerRef.current.addEventListener("mousemove", (e) => {
const rect = canvasContainerRef.current?.getBoundingClientRect()
if (rect) {
mouse.current = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
}
}
})
}
}
const resizeCanvas = () => {
if (canvasContainerRef.current && canvasRef.current && context.current) {
circles.current = []
canvasSize.current.w = canvasContainerRef.current.offsetWidth
canvasSize.current.h = canvasContainerRef.current.offsetHeight
canvasRef.current.width = canvasSize.current.w * dpr
canvasRef.current.height = canvasSize.current.h * dpr
canvasRef.current.style.width = `${canvasSize.current.w}px`
canvasRef.current.style.height = `${canvasSize.current.h}px`
context.current.scale(dpr, dpr)
}
}
const circleParams = () => {
const x = Math.floor(Math.random() * canvasSize.current.w)
const y = Math.floor(Math.random() * canvasSize.current.h)
const translateX = 0
const translateY = 0
const pSize = Math.floor(Math.random() * 2) + size
const alpha = 0
const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1))
const dx = (Math.random() - 0.5) * 0.2
const dy = (Math.random() - 0.5) * 0.2
const magnetism = 0.1 + Math.random() * 4
return {
x,
y,
translateX,
translateY,
size: pSize,
alpha,
targetAlpha,
dx,
dy,
magnetism,
}
}
const drawParticles = () => {
circles.current = []
for (let i = 0; i < quantity; i++) {
circles.current.push(circleParams())
}
}
const clearContext = () => {
if (context.current) {
context.current.clearRect(
0,
0,
canvasSize.current.w,
canvasSize.current.h
)
}
}
const drawCircle = (circle: any, update = false) => {
if (context.current) {
const { x, y, translateX, translateY, size, alpha } = circle
context.current.translate(translateX, translateY)
context.current.beginPath()
context.current.arc(x, y, size, 0, 2 * Math.PI)
context.current.fillStyle = `${color}${Math.floor(alpha * 255)
.toString(16)
.padStart(2, "0")}`
context.current.fill()
context.current.setTransform(dpr, 0, 0, dpr, 0, 0)
if (!update) {
circles.current.push(circle)
}
}
}
const animate = () => {
clearContext()
circles.current.forEach((circle, i) => {
// Handle alpha
const edge = [
circle.x + circle.translateX - circle.size,
canvasSize.current.w - circle.x - circle.translateX - circle.size,
circle.y + circle.translateY - circle.size,
canvasSize.current.h - circle.y - circle.translateY - circle.size,
]
const closestEdge = edge.reduce((a, b) => Math.min(a, b))
const remapClosestEdge = parseFloat(
remapValue(closestEdge, 0, 20, 0, 1).toFixed(2)
)
if (remapClosestEdge > 1) {
circle.alpha += 0.02
if (circle.alpha > circle.targetAlpha) {
circle.alpha = circle.targetAlpha
}
} else {
circle.alpha = circle.targetAlpha * remapClosestEdge
}
circle.x += circle.dx
circle.y += circle.dy
circle.translateX +=
(mouse.current.x / (staticity / circle.magnetism) - circle.translateX) /
ease
circle.translateY +=
(mouse.current.y / (staticity / circle.magnetism) - circle.translateY) /
ease
// Wrap around
if (
circle.x < -circle.size ||
circle.x > canvasSize.current.w + circle.size ||
circle.y < -circle.size ||
circle.y > canvasSize.current.h + circle.size
) {
circles.current.splice(i, 1)
circles.current.push(circleParams())
} else {
drawCircle(circle, true)
}
})
window.requestAnimationFrame(animate)
}
const remapValue = (
value: number,
start1: number,
end1: number,
start2: number,
end2: number
) => {
const remapped =
((value - start1) * (end2 - start2)) / (end1 - start1) + start2
return remapped > 0 ? remapped : 0
}
return (
<div
ref={canvasContainerRef}
className={cn("pointer-events-auto h-full w-full", className)}
>
<canvas ref={canvasRef} className="h-full w-full" />
</div>
)
}
// Użycie
<div className="relative h-screen w-full bg-black">
<Particles
className="absolute inset-0"
quantity={100}
color="#ffffff"
size={1}
/>
<div className="relative z-10">
{/* Your content */}
</div>
</div>Dot Pattern
// components/magicui/dot-pattern.tsx
import { cn } from "@/lib/utils"
interface DotPatternProps {
width?: number
height?: number
x?: number
y?: number
cx?: number
cy?: number
cr?: number
className?: string
}
export function DotPattern({
width = 16,
height = 16,
x = 0,
y = 0,
cx = 1,
cy = 1,
cr = 1,
className,
...props
}: DotPatternProps) {
const id = `pattern-${Math.random().toString(36).substr(2, 9)}`
return (
<svg
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 h-full w-full fill-neutral-400/80",
className
)}
{...props}
>
<defs>
<pattern
id={id}
width={width}
height={height}
patternUnits="userSpaceOnUse"
patternContentUnits="userSpaceOnUse"
x={x}
y={y}
>
<circle id="pattern-circle" cx={cx} cy={cy} r={cr} />
</pattern>
</defs>
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
</svg>
)
}
// Użycie
<div className="relative h-screen">
<DotPattern
className="[mask-image:radial-gradient(400px_circle_at_center,white,transparent)]"
/>
<div className="relative z-10">{/* Content */}</div>
</div>Grid Pattern
// components/magicui/grid-pattern.tsx
import { cn } from "@/lib/utils"
interface GridPatternProps {
width?: number
height?: number
x?: number
y?: number
squares?: number[][]
strokeDasharray?: string
className?: string
}
export function GridPattern({
width = 40,
height = 40,
x = -1,
y = -1,
squares,
strokeDasharray = "0",
className,
...props
}: GridPatternProps) {
const id = `grid-${Math.random().toString(36).substr(2, 9)}`
return (
<svg
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-gray-400/30",
className
)}
{...props}
>
<defs>
<pattern
id={id}
width={width}
height={height}
patternUnits="userSpaceOnUse"
x={x}
y={y}
>
<path
d={`M.5 ${height}V.5H${width}`}
fill="none"
strokeDasharray={strokeDasharray}
/>
</pattern>
</defs>
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
{squares && (
<svg x={x} y={y} className="overflow-visible">
{squares.map(([squareX, squareY]) => (
<rect
strokeWidth="0"
key={`${squareX}-${squareY}`}
width={width - 1}
height={height - 1}
x={squareX * width + 1}
y={squareY * height + 1}
/>
))}
</svg>
)}
</svg>
)
}
// Użycie z highlighted squares
<GridPattern
squares={[
[1, 1],
[3, 2],
[5, 4],
]}
className="[mask-image:radial-gradient(500px_circle_at_center,white,transparent)]"
/>Meteors
// components/magicui/meteors.tsx
import { cn } from "@/lib/utils"
interface MeteorsProps {
number?: number
className?: string
}
export function Meteors({ number = 20, className }: MeteorsProps) {
const meteors = new Array(number).fill(true)
return (
<>
{meteors.map((_, idx) => (
<span
key={idx}
className={cn(
"animate-meteor 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:transform 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() * 100)}%`,
animationDelay: `${Math.random() * 1}s`,
animationDuration: `${Math.floor(Math.random() * 8 + 2)}s`,
}}
/>
))}
</>
)
}
// Użycie
<div className="relative h-screen overflow-hidden bg-slate-950">
<Meteors number={30} />
<div className="relative z-10">{/* Content */}</div>
</div>Komponenty - Cards & Layouts
Bento Grid
// components/magicui/bento-grid.tsx
import { cn } from "@/lib/utils"
interface BentoGridProps {
className?: string
children?: React.ReactNode
}
export function BentoGrid({ className, children }: BentoGridProps) {
return (
<div
className={cn(
"grid w-full auto-rows-[22rem] grid-cols-3 gap-4",
className
)}
>
{children}
</div>
)
}
interface BentoCardProps {
name: string
className?: string
background?: React.ReactNode
Icon?: any
description: string
href?: string
cta?: string
}
export function BentoCard({
name,
className,
background,
Icon,
description,
href,
cta,
}: BentoCardProps) {
return (
<div
className={cn(
"group relative col-span-1 flex flex-col justify-between overflow-hidden rounded-xl",
"bg-white [box-shadow:0_0_0_1px_rgba(0,0,0,.03),0_2px_4px_rgba(0,0,0,.05),0_12px_24px_rgba(0,0,0,.05)]",
"transform-gpu dark:bg-black dark:[border:1px_solid_rgba(255,255,255,.1)] dark:[box-shadow:0_-20px_80px_-20px_#ffffff1f_inset]",
className
)}
>
{/* Background */}
<div className="absolute inset-0">{background}</div>
{/* Content */}
<div className="pointer-events-none z-10 flex transform-gpu flex-col gap-1 p-6 transition-all duration-300 group-hover:-translate-y-10">
{Icon && (
<Icon className="h-12 w-12 origin-left transform-gpu text-neutral-700 transition-all duration-300 ease-in-out group-hover:scale-75" />
)}
<h3 className="text-xl font-semibold text-neutral-700 dark:text-neutral-300">
{name}
</h3>
<p className="max-w-lg text-neutral-400">{description}</p>
</div>
{/* CTA */}
<div
className={cn(
"pointer-events-none absolute bottom-0 flex w-full translate-y-10 transform-gpu flex-row items-center p-4 opacity-0 transition-all duration-300 group-hover:translate-y-0 group-hover:opacity-100"
)}
>
{href && (
<a
href={href}
className="pointer-events-auto inline-flex items-center gap-1 text-sm font-medium text-neutral-700 dark:text-neutral-300"
>
{cta}
<span className="ml-1">→</span>
</a>
)}
</div>
{/* Hover effect */}
<div className="pointer-events-none absolute inset-0 transform-gpu transition-all duration-300 group-hover:bg-black/[.03] group-hover:dark:bg-neutral-800/10" />
</div>
)
}
// Użycie
<BentoGrid className="lg:grid-rows-3">
<BentoCard
name="Feature 1"
className="lg:col-span-2 lg:row-span-2"
Icon={RocketIcon}
description="Description of feature 1"
href="/feature-1"
cta="Learn more"
background={<Particles />}
/>
<BentoCard
name="Feature 2"
className="lg:col-span-1"
Icon={SparklesIcon}
description="Description of feature 2"
href="/feature-2"
cta="Explore"
/>
</BentoGrid>Spotlight Card
// components/magicui/spotlight-card.tsx
"use client"
import { useRef, useState } from "react"
import { cn } from "@/lib/utils"
interface SpotlightCardProps {
children: React.ReactNode
className?: string
}
export function SpotlightCard({ children, className }: SpotlightCardProps) {
const divRef = useRef<HTMLDivElement>(null)
const [position, setPosition] = useState({ x: 0, y: 0 })
const [opacity, setOpacity] = useState(0)
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!divRef.current) return
const rect = divRef.current.getBoundingClientRect()
setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top })
}
const handleMouseEnter = () => {
setOpacity(1)
}
const handleMouseLeave = () => {
setOpacity(0)
}
return (
<div
ref={divRef}
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={cn(
"relative overflow-hidden rounded-xl border border-slate-800 bg-gradient-to-r from-black to-slate-950 p-8",
className
)}
>
{/* Spotlight effect */}
<div
className="pointer-events-none absolute -inset-px opacity-0 transition duration-300"
style={{
opacity,
background: `radial-gradient(600px circle at ${position.x}px ${position.y}px, rgba(255,182,255,.1), transparent 40%)`,
}}
/>
{children}
</div>
)
}
// Użycie
<SpotlightCard>
<h2 className="text-xl font-bold text-white">Premium Feature</h2>
<p className="text-slate-400">
Hover over this card to see the spotlight effect
</p>
</SpotlightCard>Komponenty - Special Effects
Animated Beam
// components/magicui/animated-beam.tsx
"use client"
import { RefObject, useEffect, useState } from "react"
import { motion } from "framer-motion"
import { cn } from "@/lib/utils"
interface AnimatedBeamProps {
containerRef: RefObject<HTMLElement>
fromRef: RefObject<HTMLElement>
toRef: RefObject<HTMLElement>
curvature?: number
endYOffset?: number
reverse?: boolean
pathColor?: string
pathWidth?: number
pathOpacity?: number
gradientStartColor?: string
gradientStopColor?: string
delay?: number
duration?: number
className?: string
}
export function AnimatedBeam({
containerRef,
fromRef,
toRef,
curvature = 0,
endYOffset = 0,
reverse = false,
pathColor = "gray",
pathWidth = 2,
pathOpacity = 0.2,
gradientStartColor = "#ffaa40",
gradientStopColor = "#9c40ff",
delay = 0,
duration = Math.random() * 3 + 4,
className,
}: AnimatedBeamProps) {
const [pathD, setPathD] = useState("")
const [svgDimensions, setSvgDimensions] = useState({ width: 0, height: 0 })
const id = `beam-${Math.random().toString(36).substr(2, 9)}`
useEffect(() => {
const updatePath = () => {
if (containerRef.current && fromRef.current && toRef.current) {
const containerRect = containerRef.current.getBoundingClientRect()
const fromRect = fromRef.current.getBoundingClientRect()
const toRect = toRef.current.getBoundingClientRect()
const svgWidth = containerRect.width
const svgHeight = containerRect.height
setSvgDimensions({ width: svgWidth, height: svgHeight })
const startX = fromRect.left - containerRect.left + fromRect.width / 2
const startY = fromRect.top - containerRect.top + fromRect.height / 2
const endX = toRect.left - containerRect.left + toRect.width / 2
const endY =
toRect.top - containerRect.top + toRect.height / 2 + endYOffset
const controlY = startY - curvature
const d = `M ${startX},${startY} Q ${
(startX + endX) / 2
},${controlY} ${endX},${endY}`
setPathD(d)
}
}
updatePath()
window.addEventListener("resize", updatePath)
return () => {
window.removeEventListener("resize", updatePath)
}
}, [containerRef, fromRef, toRef, curvature, endYOffset])
return (
<svg
fill="none"
width={svgDimensions.width}
height={svgDimensions.height}
xmlns="http://www.w3.org/2000/svg"
className={cn(
"pointer-events-none absolute left-0 top-0 transform-gpu stroke-2",
className
)}
>
{/* Background path */}
<path
d={pathD}
stroke={pathColor}
strokeWidth={pathWidth}
strokeOpacity={pathOpacity}
strokeLinecap="round"
/>
{/* Animated gradient path */}
<path
d={pathD}
strokeWidth={pathWidth}
stroke={`url(#${id})`}
strokeOpacity="1"
strokeLinecap="round"
/>
<defs>
<motion.linearGradient
id={id}
gradientUnits="userSpaceOnUse"
initial={{
x1: reverse ? "100%" : "0%",
x2: reverse ? "100%" : "0%",
y1: "0%",
y2: "0%",
}}
animate={{
x1: reverse ? "-20%" : "120%",
x2: reverse ? "0%" : "100%",
}}
transition={{
delay,
duration,
ease: [0.16, 1, 0.3, 1],
repeat: Infinity,
repeatDelay: 0,
}}
>
<stop stopColor={gradientStartColor} stopOpacity="0" />
<stop stopColor={gradientStartColor} />
<stop offset="0.325" stopColor={gradientStopColor} />
<stop offset="1" stopColor={gradientStopColor} stopOpacity="0" />
</motion.linearGradient>
</defs>
</svg>
)
}
// Użycie - połączenie dwóch elementów
const containerRef = useRef<HTMLDivElement>(null)
const div1Ref = useRef<HTMLDivElement>(null)
const div2Ref = useRef<HTMLDivElement>(null)
<div ref={containerRef} className="relative">
<div ref={div1Ref} className="...">Element 1</div>
<div ref={div2Ref} className="...">Element 2</div>
<AnimatedBeam
containerRef={containerRef}
fromRef={div1Ref}
toRef={div2Ref}
curvature={-50}
/>
</div>Globe (3D)
// components/magicui/globe.tsx
"use client"
import { useEffect, useRef } from "react"
import createGlobe from "cobe"
import { cn } from "@/lib/utils"
interface GlobeProps {
className?: string
config?: {
width?: number
height?: number
phi?: number
theta?: number
dark?: number
diffuse?: number
mapSamples?: number
mapBrightness?: number
baseColor?: [number, number, number]
markerColor?: [number, number, number]
glowColor?: [number, number, number]
markers?: { location: [number, number]; size: number }[]
}
}
const defaultConfig = {
width: 800,
height: 800,
phi: 0,
theta: 0,
dark: 1,
diffuse: 1.2,
mapSamples: 16000,
mapBrightness: 6,
baseColor: [0.3, 0.3, 0.3] as [number, number, number],
markerColor: [0.1, 0.8, 1] as [number, number, number],
glowColor: [0.1, 0.1, 0.1] as [number, number, number],
markers: [
{ location: [52.52, 13.405], size: 0.05 }, // Berlin
{ location: [40.7128, -74.006], size: 0.07 }, // NYC
{ location: [35.6762, 139.6503], size: 0.06 }, // Tokyo
{ location: [51.5074, -0.1278], size: 0.05 }, // London
],
}
export function Globe({ className, config = {} }: GlobeProps) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const mergedConfig = { ...defaultConfig, ...config }
useEffect(() => {
let phi = mergedConfig.phi
let width = 0
const onResize = () => {
if (canvasRef.current) {
width = canvasRef.current.offsetWidth
}
}
window.addEventListener("resize", onResize)
onResize()
const globe = createGlobe(canvasRef.current!, {
devicePixelRatio: 2,
width: mergedConfig.width * 2,
height: mergedConfig.height * 2,
phi: mergedConfig.phi,
theta: mergedConfig.theta,
dark: mergedConfig.dark,
diffuse: mergedConfig.diffuse,
mapSamples: mergedConfig.mapSamples,
mapBrightness: mergedConfig.mapBrightness,
baseColor: mergedConfig.baseColor,
markerColor: mergedConfig.markerColor,
glowColor: mergedConfig.glowColor,
markers: mergedConfig.markers,
onRender: (state) => {
// Auto-rotate
state.phi = phi
phi += 0.005
},
})
return () => {
globe.destroy()
window.removeEventListener("resize", onResize)
}
}, [])
return (
<canvas
ref={canvasRef}
className={cn("h-full w-full", className)}
style={{
width: mergedConfig.width,
height: mergedConfig.height,
maxWidth: "100%",
aspectRatio: 1,
}}
/>
)
}
// Użycie
// npm install cobe
<Globe
className="mx-auto"
config={{
width: 500,
height: 500,
markers: [
{ location: [52.23, 21.01], size: 0.08 }, // Warsaw
],
}}
/>Przykładowa strona Landing
// app/page.tsx
import { ShimmerButton } from "@/components/magicui/shimmer-button"
import { TypingAnimation } from "@/components/magicui/typing-animation"
import { GradientText } from "@/components/magicui/gradient-text"
import { Particles } from "@/components/magicui/particles"
import { BentoGrid, BentoCard } from "@/components/magicui/bento-grid"
import { Globe } from "@/components/magicui/globe"
export default function LandingPage() {
return (
<main className="relative min-h-screen bg-black text-white">
{/* Hero Section */}
<section className="relative h-screen flex items-center justify-center overflow-hidden">
<Particles
className="absolute inset-0"
quantity={100}
color="#ffffff"
/>
<div className="relative z-10 text-center">
<TypingAnimation
text="Build Something Magic"
duration={100}
className="text-6xl font-bold mb-6"
/>
<p className="text-xl text-gray-400 mb-8 max-w-2xl mx-auto">
Create stunning <GradientText>landing pages</GradientText> with
beautiful animations and effects
</p>
<div className="flex gap-4 justify-center">
<ShimmerButton>
Get Started
</ShimmerButton>
<button className="px-6 py-3 border border-white/20 rounded-full hover:bg-white/10 transition">
Learn More
</button>
</div>
</div>
</section>
{/* Features Section */}
<section className="py-24 px-6">
<div className="max-w-6xl mx-auto">
<h2 className="text-4xl font-bold text-center mb-16">
<GradientText>Features</GradientText>
</h2>
<BentoGrid className="lg:grid-rows-3">
<BentoCard
name="Copy & Paste"
className="lg:col-span-2 lg:row-span-2"
description="Simply copy components and customize them"
href="#"
cta="Learn more"
/>
<BentoCard
name="TypeScript"
className="lg:col-span-1"
description="Full TypeScript support"
href="#"
cta="Explore"
/>
<BentoCard
name="Dark Mode"
className="lg:col-span-1"
description="Native dark mode support"
href="#"
cta="See"
/>
<BentoCard
name="Responsive"
className="lg:col-span-2"
description="Works on all screen sizes"
href="#"
cta="Demo"
/>
</BentoGrid>
</div>
</section>
{/* Globe Section */}
<section className="py-24 relative">
<div className="absolute inset-0 flex items-center justify-center">
<Globe className="opacity-50" />
</div>
<div className="relative z-10 text-center">
<h2 className="text-4xl font-bold mb-4">
Used Worldwide
</h2>
<p className="text-gray-400">
Developers around the globe trust Magic UI
</p>
</div>
</section>
{/* CTA Section */}
<section className="py-24 text-center">
<h2 className="text-4xl font-bold mb-6">
Ready to get started?
</h2>
<ShimmerButton
shimmerColor="#ffffff"
background="linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
>
Start Building Today
</ShimmerButton>
</section>
</main>
)
}FAQ - Najczęściej zadawane pytania
Czy Magic UI jest darmowe?
Tak, Magic UI jest w pełni darmowe i open-source na licencji MIT. Możesz używać komponentów w projektach komercyjnych bez żadnych ograniczeń.
Czy muszę instalować pakiet npm?
Nie, Magic UI używa podejścia copy-paste. Kopiujesz kod komponentu do swojego projektu. Możesz też użyć shadcn CLI do automatycznego dodawania komponentów.
Jakie są wymagania?
- React 18+
- Tailwind CSS
- Framer Motion (
npm install framer-motion) - clsx i tailwind-merge dla utility function cn()
Czy Magic UI działa z Next.js?
Tak, Magic UI doskonale współpracuje z Next.js (zarówno Pages Router jak i App Router). Niektóre komponenty wymagają dyrektywy "use client".
Czy mogę modyfikować komponenty?
Tak, to główna zaleta podejścia copy-paste. Kod jest Twój - możesz go dowolnie modyfikować, rozszerzać i dostosowywać do swoich potrzeb.
Jak dodać ciemny motyw?
Magic UI wspiera natywnie ciemny motyw przez klasy Tailwind dark:. Upewnij się, że masz skonfigurowany darkMode: ["class"] w tailwind.config.js.
Czy komponenty są dostępne (accessible)?
Magic UI stara się zachować podstawową dostępność, ale ze względu na dekoracyjny charakter niektórych efektów, pełna zgodność z WCAG nie jest zagwarantowana. Animacje można wyłączyć przez prefers-reduced-motion.
Jak zgłosić błąd lub zaproponować nowy komponent?
Możesz otworzyć issue na GitHub Magic UI lub przesłać Pull Request z nowym komponentem.
Podsumowanie
Magic UI to kolekcja pięknych, animowanych komponentów React, które pozwalają tworzyć wyróżniające się landing pages i aplikacje. Dzięki podejściu copy-paste masz pełną kontrolę nad kodem, a integracja z Tailwind CSS i Framer Motion zapewnia profesjonalne animacje bez boilerplate.
Główne zalety Magic UI:
- Copy-paste - zero zależności, pełna kontrola
- Piękne animacje - shimmer, particles, beams, globe
- TypeScript - kompletne typy
- Tailwind + Framer Motion - znane technologie
- Dark mode - natywne wsparcie
- shadcn compatible - łatwa integracja
Czy to dla landing page SaaS, portfolio dewelopera, czy strony produktowej - Magic UI dostarcza komponenty, które robią wrażenie.
Magic UI - complete guide to animated React components
What is Magic UI?
Magic UI is a collection of beautiful, ready-to-use animated React components that you can copy and paste into your project. Built on the foundation of shadcn/ui and Framer Motion, Magic UI components add "magic" to landing pages, portfolios, and SaaS applications without the need to write complex animations from scratch.
Unlike traditional component libraries, Magic UI is not an npm package to install. Instead, each component is available as source code that you copy into your project. This "copy-paste" philosophy - pioneered by shadcn/ui - gives you full control over the code and the ability to customize it to your needs.
Magic UI offers components you won't find in standard UI libraries: animated beams connecting elements (like in integration diagrams), shimmer buttons like Apple's, interactive particles that react to the cursor, spotlight effects, meteors, typing animations, and much more. Perfect for creating standout landing pages and product presentations.
Philosophy and approach
Magic UI was born from the observation that the most beautiful websites (like those from Stripe, Linear, Vercel) use custom animations that are difficult to reproduce. The creators of Magic UI decided to collect these effects and make them available as ready-made React components.
Key principles of Magic UI:
- Copy-paste, not install - full control over the code
- Framer Motion - professional animations without boilerplate
- Tailwind CSS - consistent styling
- shadcn/ui compatible - integration with a popular system
- TypeScript - complete types for better DX
Why Magic UI?
Key advantages
- Zero dependencies - only Framer Motion and Tailwind
- Copy-paste - full control, zero vendor lock-in
- TypeScript - complete types for all components
- Customizable - the code is yours, change it however you want
- Performance - optimized animations
- Responsive - works on all devices
- Dark mode - native support
- Accessible - WCAG compliant where possible
Magic UI vs Aceternity UI vs Framer Motion
| Feature | Magic UI | Aceternity UI | Framer Motion |
|---|---|---|---|
| Type | Components | Components | Library |
| Installation | Copy-paste | Copy-paste | npm install |
| Customize | Full control | Full control | Programming |
| Components | ~50+ | ~40+ | Build your own |
| Learning curve | Low | Low | Medium |
| TypeScript | ✅ | ✅ | ✅ |
| Dark mode | ✅ | ✅ | N/A |
| shadcn compatible | ✅ | ✅ | N/A |
| License | MIT | MIT | MIT |
Installation and setup
Requirements
# Your project should have:
- React 18+
- Tailwind CSS
- Framer Motion
- (optionally) shadcn/ui
# Installing dependencies
npm install framer-motion clsx tailwind-mergeTailwind configuration
// tailwind.config.js
module.exports = {
darkMode: ["class"],
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
],
theme: {
extend: {
animation: {
"shimmer": "shimmer 2s linear infinite",
"spin-slow": "spin 3s linear infinite",
"pulse-slow": "pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite",
"bounce-slow": "bounce 2s infinite",
"meteor": "meteor 5s linear infinite",
"beam": "beam 2s ease-in-out infinite",
},
keyframes: {
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,
},
},
beam: {
"0%": { transform: "translateY(0)" },
"50%": { transform: "translateY(-10px)" },
"100%": { transform: "translateY(0)" },
},
},
},
},
plugins: [require("tailwindcss-animate")],
}Utility functions
// lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}Adding components
# Method 1: shadcn CLI (if you have it configured)
npx shadcn@latest add "https://magicui.design/r/shimmer-button"
# Method 2: Manual copy
# 1. Go to magicui.design
# 2. Choose a component
# 3. Click "Copy"
# 4. Paste into components/magicui/[name].tsxComponents - buttons
Shimmer Button
// components/magicui/shimmer-button.tsx
import { cn } from "@/lib/utils"
import { motion } from "framer-motion"
interface ShimmerButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
shimmerColor?: string
shimmerSize?: string
borderRadius?: string
shimmerDuration?: string
background?: string
className?: string
children?: React.ReactNode
}
export function ShimmerButton({
shimmerColor = "#ffffff",
shimmerSize = "0.05em",
shimmerDuration = "3s",
borderRadius = "100px",
background = "rgba(0, 0, 0, 1)",
className,
children,
...props
}: ShimmerButtonProps) {
return (
<button
style={{
"--shimmer-color": shimmerColor,
"--shimmer-size": shimmerSize,
"--shimmer-duration": shimmerDuration,
"--border-radius": borderRadius,
"--background": background,
} as React.CSSProperties}
className={cn(
"group relative z-0 flex cursor-pointer items-center justify-center overflow-hidden whitespace-nowrap border border-white/10 px-6 py-3 text-white [background:var(--background)] [border-radius:var(--border-radius)]",
"transform-gpu transition-transform duration-300 ease-in-out active:translate-y-[1px]",
className
)}
{...props}
>
<div
className={cn(
"absolute inset-0 overflow-hidden [border-radius:var(--border-radius)]",
"before:absolute before:inset-[-100%] before:h-[300%] before:w-[50%]",
"before:animate-[shimmer_var(--shimmer-duration)_linear_infinite]",
"before:bg-gradient-to-r before:from-transparent before:via-[var(--shimmer-color)] before:to-transparent",
"before:opacity-50"
)}
/>
<span className="relative z-10 flex items-center gap-2">
{children}
</span>
<div
className={cn(
"absolute inset-0 -z-10 blur-xl transition-opacity duration-500",
"bg-gradient-to-br from-purple-500/30 via-pink-500/30 to-blue-500/30",
"opacity-0 group-hover:opacity-100"
)}
/>
</button>
)
}
// Usage
export default function Hero() {
return (
<ShimmerButton className="shadow-2xl">
<span className="whitespace-pre-wrap text-center text-sm font-medium leading-none tracking-tight text-white lg:text-lg">
Get Started
</span>
</ShimmerButton>
)
}Pulsating Button
// components/magicui/pulsating-button.tsx
import { cn } from "@/lib/utils"
interface PulsatingButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
pulseColor?: string
duration?: string
className?: string
children?: React.ReactNode
}
export function PulsatingButton({
pulseColor = "#0096ff",
duration = "1.5s",
className,
children,
...props
}: PulsatingButtonProps) {
return (
<button
className={cn(
"relative flex cursor-pointer items-center justify-center rounded-lg bg-blue-500 px-6 py-3 text-center text-white",
className
)}
style={{
"--pulse-color": pulseColor,
"--duration": duration,
} as React.CSSProperties}
{...props}
>
<div className="absolute -inset-1 rounded-lg bg-[var(--pulse-color)] opacity-75 blur animate-pulse" />
<span className="relative flex items-center gap-2">
{children}
</span>
</button>
)
}
// Usage
<PulsatingButton pulseColor="#7c3aed">
Subscribe Now
</PulsatingButton>Animated Subscribe Button
// components/magicui/animated-subscribe-button.tsx
import { AnimatePresence, motion } from "framer-motion"
import { useState } from "react"
import { cn } from "@/lib/utils"
import { CheckIcon } from "lucide-react"
interface AnimatedSubscribeButtonProps {
buttonColor?: string
buttonTextColor?: string
subscribeStatus?: boolean
initialText: React.ReactNode
changeText: React.ReactNode
className?: string
}
export function AnimatedSubscribeButton({
buttonColor = "#000000",
buttonTextColor = "#ffffff",
subscribeStatus = false,
initialText,
changeText,
className,
}: AnimatedSubscribeButtonProps) {
const [isSubscribed, setIsSubscribed] = useState(subscribeStatus)
return (
<AnimatePresence mode="wait">
{isSubscribed ? (
<motion.button
key="subscribed"
className={cn(
"relative flex items-center justify-center overflow-hidden rounded-lg p-[10px] outline-none",
className
)}
style={{ backgroundColor: buttonColor, color: buttonTextColor }}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
onClick={() => setIsSubscribed(false)}
>
<motion.span
key="checkmark"
className="flex items-center gap-2"
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.1 }}
>
<CheckIcon className="h-4 w-4" />
{changeText}
</motion.span>
</motion.button>
) : (
<motion.button
key="not-subscribed"
className={cn(
"relative flex cursor-pointer items-center justify-center rounded-lg border-none p-[10px]",
className
)}
style={{ backgroundColor: buttonColor, color: buttonTextColor }}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
onClick={() => setIsSubscribed(true)}
>
<motion.span
key="reaction"
className="flex items-center gap-2"
initial={{ x: 0 }}
exit={{ x: 50, transition: { duration: 0.1 } }}
>
{initialText}
</motion.span>
</motion.button>
)}
</AnimatePresence>
)
}
// Usage
<AnimatedSubscribeButton
buttonColor="#000000"
buttonTextColor="#ffffff"
subscribeStatus={false}
initialText="Subscribe"
changeText="Subscribed!"
/>Components - text animations
Typing Animation
// components/magicui/typing-animation.tsx
"use client"
import { useEffect, useState } from "react"
import { cn } from "@/lib/utils"
interface TypingAnimationProps {
text: string
duration?: number
className?: string
}
export function TypingAnimation({
text,
duration = 100,
className,
}: TypingAnimationProps) {
const [displayedText, setDisplayedText] = useState("")
const [index, setIndex] = useState(0)
useEffect(() => {
const typingEffect = setInterval(() => {
if (index < text.length) {
setDisplayedText((prev) => prev + text.charAt(index))
setIndex((prev) => prev + 1)
} else {
clearInterval(typingEffect)
}
}, duration)
return () => clearInterval(typingEffect)
}, [duration, index, text])
return (
<h1
className={cn(
"font-display text-center text-4xl font-bold leading-[5rem] tracking-[-0.02em] drop-shadow-sm",
className
)}
>
{displayedText}
<span className="animate-pulse">|</span>
</h1>
)
}
// Usage
<TypingAnimation
text="Welcome to Magic UI"
duration={50}
className="text-6xl text-white"
/>Text Reveal
// components/magicui/text-reveal.tsx
"use client"
import { motion, useScroll, useTransform } from "framer-motion"
import { useRef } from "react"
import { cn } from "@/lib/utils"
interface TextRevealProps {
text: string
className?: string
}
export function TextReveal({ text, className }: TextRevealProps) {
const containerRef = useRef<HTMLDivElement>(null)
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start 0.9", "start 0.25"],
})
const words = text.split(" ")
return (
<div
ref={containerRef}
className={cn(
"relative z-0 mx-auto h-[50vh] max-w-4xl",
className
)}
>
<div className="sticky top-0 flex h-1/2 items-center">
<p className="flex flex-wrap text-2xl font-bold text-black/20 dark:text-white/20 md:text-3xl lg:text-4xl xl:text-5xl">
{words.map((word, i) => {
const start = i / words.length
const end = start + 1 / words.length
return (
<Word key={i} progress={scrollYProgress} range={[start, end]}>
{word}
</Word>
)
})}
</p>
</div>
</div>
)
}
interface WordProps {
children: string
progress: any
range: [number, number]
}
function Word({ children, progress, range }: WordProps) {
const opacity = useTransform(progress, range, [0, 1])
return (
<span className="relative mx-1 lg:mx-2.5">
<span className="absolute opacity-30">{children}</span>
<motion.span style={{ opacity }} className="text-black dark:text-white">
{children}
</motion.span>
</span>
)
}
// Usage (requires scroll container)
<TextReveal
text="Magic UI brings your landing pages to life with beautiful animations"
/>Gradient Text
// components/magicui/gradient-text.tsx
import { cn } from "@/lib/utils"
interface GradientTextProps {
children: React.ReactNode
className?: string
colors?: string[]
animationSpeed?: number
}
export function GradientText({
children,
className,
colors = ["#ffaa40", "#9c40ff", "#ffaa40"],
animationSpeed = 8,
}: GradientTextProps) {
const gradientStyle = {
backgroundImage: `linear-gradient(to right, ${colors.join(", ")})`,
backgroundSize: "300% 100%",
animation: `gradient ${animationSpeed}s ease infinite`,
}
return (
<>
<style>{`
@keyframes gradient {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
`}</style>
<span
className={cn(
"bg-clip-text text-transparent",
className
)}
style={gradientStyle}
>
{children}
</span>
</>
)
}
// Usage
<h1 className="text-6xl font-bold">
<GradientText>Magic UI</GradientText>
</h1>Word Rotate
// components/magicui/word-rotate.tsx
"use client"
import { AnimatePresence, motion } from "framer-motion"
import { useEffect, useState } from "react"
import { cn } from "@/lib/utils"
interface WordRotateProps {
words: string[]
duration?: number
framerProps?: any
className?: string
}
export function WordRotate({
words,
duration = 2500,
framerProps = {
initial: { opacity: 0, y: -50 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: 50 },
transition: { duration: 0.25, ease: "easeOut" },
},
className,
}: WordRotateProps) {
const [index, setIndex] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setIndex((prevIndex) => (prevIndex + 1) % words.length)
}, duration)
return () => clearInterval(interval)
}, [words, duration])
return (
<div className="overflow-hidden py-2">
<AnimatePresence mode="wait">
<motion.h1
key={words[index]}
className={cn("font-bold", className)}
{...framerProps}
>
{words[index]}
</motion.h1>
</AnimatePresence>
</div>
)
}
// Usage
<div className="text-4xl">
Build{" "}
<WordRotate
words={["beautiful", "amazing", "stunning", "magical"]}
className="text-blue-500"
/>{" "}
websites
</div>Components - backgrounds
Particles
// components/magicui/particles.tsx
"use client"
import { useEffect, useRef, useState } from "react"
import { cn } from "@/lib/utils"
interface ParticlesProps {
className?: string
quantity?: number
staticity?: number
ease?: number
refresh?: boolean
color?: string
size?: number
}
export function Particles({
className,
quantity = 50,
staticity = 50,
ease = 50,
refresh = false,
color = "#ffffff",
size = 1,
}: ParticlesProps) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const canvasContainerRef = useRef<HTMLDivElement>(null)
const context = useRef<CanvasRenderingContext2D | null>(null)
const circles = useRef<any[]>([])
const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 })
const canvasSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 })
const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1
useEffect(() => {
if (canvasRef.current) {
context.current = canvasRef.current.getContext("2d")
}
initCanvas()
animate()
window.addEventListener("resize", initCanvas)
return () => {
window.removeEventListener("resize", initCanvas)
}
}, [color])
useEffect(() => {
onMouseMove()
}, [])
const initCanvas = () => {
resizeCanvas()
drawParticles()
}
const onMouseMove = () => {
if (canvasContainerRef.current) {
canvasContainerRef.current.addEventListener("mousemove", (e) => {
const rect = canvasContainerRef.current?.getBoundingClientRect()
if (rect) {
mouse.current = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
}
}
})
}
}
const resizeCanvas = () => {
if (canvasContainerRef.current && canvasRef.current && context.current) {
circles.current = []
canvasSize.current.w = canvasContainerRef.current.offsetWidth
canvasSize.current.h = canvasContainerRef.current.offsetHeight
canvasRef.current.width = canvasSize.current.w * dpr
canvasRef.current.height = canvasSize.current.h * dpr
canvasRef.current.style.width = `${canvasSize.current.w}px`
canvasRef.current.style.height = `${canvasSize.current.h}px`
context.current.scale(dpr, dpr)
}
}
const circleParams = () => {
const x = Math.floor(Math.random() * canvasSize.current.w)
const y = Math.floor(Math.random() * canvasSize.current.h)
const translateX = 0
const translateY = 0
const pSize = Math.floor(Math.random() * 2) + size
const alpha = 0
const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1))
const dx = (Math.random() - 0.5) * 0.2
const dy = (Math.random() - 0.5) * 0.2
const magnetism = 0.1 + Math.random() * 4
return {
x,
y,
translateX,
translateY,
size: pSize,
alpha,
targetAlpha,
dx,
dy,
magnetism,
}
}
const drawParticles = () => {
circles.current = []
for (let i = 0; i < quantity; i++) {
circles.current.push(circleParams())
}
}
const clearContext = () => {
if (context.current) {
context.current.clearRect(
0,
0,
canvasSize.current.w,
canvasSize.current.h
)
}
}
const drawCircle = (circle: any, update = false) => {
if (context.current) {
const { x, y, translateX, translateY, size, alpha } = circle
context.current.translate(translateX, translateY)
context.current.beginPath()
context.current.arc(x, y, size, 0, 2 * Math.PI)
context.current.fillStyle = `${color}${Math.floor(alpha * 255)
.toString(16)
.padStart(2, "0")}`
context.current.fill()
context.current.setTransform(dpr, 0, 0, dpr, 0, 0)
if (!update) {
circles.current.push(circle)
}
}
}
const animate = () => {
clearContext()
circles.current.forEach((circle, i) => {
const edge = [
circle.x + circle.translateX - circle.size,
canvasSize.current.w - circle.x - circle.translateX - circle.size,
circle.y + circle.translateY - circle.size,
canvasSize.current.h - circle.y - circle.translateY - circle.size,
]
const closestEdge = edge.reduce((a, b) => Math.min(a, b))
const remapClosestEdge = parseFloat(
remapValue(closestEdge, 0, 20, 0, 1).toFixed(2)
)
if (remapClosestEdge > 1) {
circle.alpha += 0.02
if (circle.alpha > circle.targetAlpha) {
circle.alpha = circle.targetAlpha
}
} else {
circle.alpha = circle.targetAlpha * remapClosestEdge
}
circle.x += circle.dx
circle.y += circle.dy
circle.translateX +=
(mouse.current.x / (staticity / circle.magnetism) - circle.translateX) /
ease
circle.translateY +=
(mouse.current.y / (staticity / circle.magnetism) - circle.translateY) /
ease
if (
circle.x < -circle.size ||
circle.x > canvasSize.current.w + circle.size ||
circle.y < -circle.size ||
circle.y > canvasSize.current.h + circle.size
) {
circles.current.splice(i, 1)
circles.current.push(circleParams())
} else {
drawCircle(circle, true)
}
})
window.requestAnimationFrame(animate)
}
const remapValue = (
value: number,
start1: number,
end1: number,
start2: number,
end2: number
) => {
const remapped =
((value - start1) * (end2 - start2)) / (end1 - start1) + start2
return remapped > 0 ? remapped : 0
}
return (
<div
ref={canvasContainerRef}
className={cn("pointer-events-auto h-full w-full", className)}
>
<canvas ref={canvasRef} className="h-full w-full" />
</div>
)
}
// Usage
<div className="relative h-screen w-full bg-black">
<Particles
className="absolute inset-0"
quantity={100}
color="#ffffff"
size={1}
/>
<div className="relative z-10">
{/* Your content */}
</div>
</div>Dot Pattern
// components/magicui/dot-pattern.tsx
import { cn } from "@/lib/utils"
interface DotPatternProps {
width?: number
height?: number
x?: number
y?: number
cx?: number
cy?: number
cr?: number
className?: string
}
export function DotPattern({
width = 16,
height = 16,
x = 0,
y = 0,
cx = 1,
cy = 1,
cr = 1,
className,
...props
}: DotPatternProps) {
const id = `pattern-${Math.random().toString(36).substr(2, 9)}`
return (
<svg
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 h-full w-full fill-neutral-400/80",
className
)}
{...props}
>
<defs>
<pattern
id={id}
width={width}
height={height}
patternUnits="userSpaceOnUse"
patternContentUnits="userSpaceOnUse"
x={x}
y={y}
>
<circle id="pattern-circle" cx={cx} cy={cy} r={cr} />
</pattern>
</defs>
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
</svg>
)
}
// Usage
<div className="relative h-screen">
<DotPattern
className="[mask-image:radial-gradient(400px_circle_at_center,white,transparent)]"
/>
<div className="relative z-10">{/* Content */}</div>
</div>Grid Pattern
// components/magicui/grid-pattern.tsx
import { cn } from "@/lib/utils"
interface GridPatternProps {
width?: number
height?: number
x?: number
y?: number
squares?: number[][]
strokeDasharray?: string
className?: string
}
export function GridPattern({
width = 40,
height = 40,
x = -1,
y = -1,
squares,
strokeDasharray = "0",
className,
...props
}: GridPatternProps) {
const id = `grid-${Math.random().toString(36).substr(2, 9)}`
return (
<svg
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-gray-400/30",
className
)}
{...props}
>
<defs>
<pattern
id={id}
width={width}
height={height}
patternUnits="userSpaceOnUse"
x={x}
y={y}
>
<path
d={`M.5 ${height}V.5H${width}`}
fill="none"
strokeDasharray={strokeDasharray}
/>
</pattern>
</defs>
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
{squares && (
<svg x={x} y={y} className="overflow-visible">
{squares.map(([squareX, squareY]) => (
<rect
strokeWidth="0"
key={`${squareX}-${squareY}`}
width={width - 1}
height={height - 1}
x={squareX * width + 1}
y={squareY * height + 1}
/>
))}
</svg>
)}
</svg>
)
}
// Usage with highlighted squares
<GridPattern
squares={[
[1, 1],
[3, 2],
[5, 4],
]}
className="[mask-image:radial-gradient(500px_circle_at_center,white,transparent)]"
/>Meteors
// components/magicui/meteors.tsx
import { cn } from "@/lib/utils"
interface MeteorsProps {
number?: number
className?: string
}
export function Meteors({ number = 20, className }: MeteorsProps) {
const meteors = new Array(number).fill(true)
return (
<>
{meteors.map((_, idx) => (
<span
key={idx}
className={cn(
"animate-meteor 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:transform 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() * 100)}%`,
animationDelay: `${Math.random() * 1}s`,
animationDuration: `${Math.floor(Math.random() * 8 + 2)}s`,
}}
/>
))}
</>
)
}
// Usage
<div className="relative h-screen overflow-hidden bg-slate-950">
<Meteors number={30} />
<div className="relative z-10">{/* Content */}</div>
</div>Components - cards & layouts
Bento Grid
// components/magicui/bento-grid.tsx
import { cn } from "@/lib/utils"
interface BentoGridProps {
className?: string
children?: React.ReactNode
}
export function BentoGrid({ className, children }: BentoGridProps) {
return (
<div
className={cn(
"grid w-full auto-rows-[22rem] grid-cols-3 gap-4",
className
)}
>
{children}
</div>
)
}
interface BentoCardProps {
name: string
className?: string
background?: React.ReactNode
Icon?: any
description: string
href?: string
cta?: string
}
export function BentoCard({
name,
className,
background,
Icon,
description,
href,
cta,
}: BentoCardProps) {
return (
<div
className={cn(
"group relative col-span-1 flex flex-col justify-between overflow-hidden rounded-xl",
"bg-white [box-shadow:0_0_0_1px_rgba(0,0,0,.03),0_2px_4px_rgba(0,0,0,.05),0_12px_24px_rgba(0,0,0,.05)]",
"transform-gpu dark:bg-black dark:[border:1px_solid_rgba(255,255,255,.1)] dark:[box-shadow:0_-20px_80px_-20px_#ffffff1f_inset]",
className
)}
>
<div className="absolute inset-0">{background}</div>
<div className="pointer-events-none z-10 flex transform-gpu flex-col gap-1 p-6 transition-all duration-300 group-hover:-translate-y-10">
{Icon && (
<Icon className="h-12 w-12 origin-left transform-gpu text-neutral-700 transition-all duration-300 ease-in-out group-hover:scale-75" />
)}
<h3 className="text-xl font-semibold text-neutral-700 dark:text-neutral-300">
{name}
</h3>
<p className="max-w-lg text-neutral-400">{description}</p>
</div>
<div
className={cn(
"pointer-events-none absolute bottom-0 flex w-full translate-y-10 transform-gpu flex-row items-center p-4 opacity-0 transition-all duration-300 group-hover:translate-y-0 group-hover:opacity-100"
)}
>
{href && (
<a
href={href}
className="pointer-events-auto inline-flex items-center gap-1 text-sm font-medium text-neutral-700 dark:text-neutral-300"
>
{cta}
<span className="ml-1">→</span>
</a>
)}
</div>
<div className="pointer-events-none absolute inset-0 transform-gpu transition-all duration-300 group-hover:bg-black/[.03] group-hover:dark:bg-neutral-800/10" />
</div>
)
}
// Usage
<BentoGrid className="lg:grid-rows-3">
<BentoCard
name="Feature 1"
className="lg:col-span-2 lg:row-span-2"
Icon={RocketIcon}
description="Description of feature 1"
href="/feature-1"
cta="Learn more"
background={<Particles />}
/>
<BentoCard
name="Feature 2"
className="lg:col-span-1"
Icon={SparklesIcon}
description="Description of feature 2"
href="/feature-2"
cta="Explore"
/>
</BentoGrid>Spotlight Card
// components/magicui/spotlight-card.tsx
"use client"
import { useRef, useState } from "react"
import { cn } from "@/lib/utils"
interface SpotlightCardProps {
children: React.ReactNode
className?: string
}
export function SpotlightCard({ children, className }: SpotlightCardProps) {
const divRef = useRef<HTMLDivElement>(null)
const [position, setPosition] = useState({ x: 0, y: 0 })
const [opacity, setOpacity] = useState(0)
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!divRef.current) return
const rect = divRef.current.getBoundingClientRect()
setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top })
}
const handleMouseEnter = () => {
setOpacity(1)
}
const handleMouseLeave = () => {
setOpacity(0)
}
return (
<div
ref={divRef}
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={cn(
"relative overflow-hidden rounded-xl border border-slate-800 bg-gradient-to-r from-black to-slate-950 p-8",
className
)}
>
<div
className="pointer-events-none absolute -inset-px opacity-0 transition duration-300"
style={{
opacity,
background: `radial-gradient(600px circle at ${position.x}px ${position.y}px, rgba(255,182,255,.1), transparent 40%)`,
}}
/>
{children}
</div>
)
}
// Usage
<SpotlightCard>
<h2 className="text-xl font-bold text-white">Premium Feature</h2>
<p className="text-slate-400">
Hover over this card to see the spotlight effect
</p>
</SpotlightCard>Components - special effects
Animated Beam
// components/magicui/animated-beam.tsx
"use client"
import { RefObject, useEffect, useState } from "react"
import { motion } from "framer-motion"
import { cn } from "@/lib/utils"
interface AnimatedBeamProps {
containerRef: RefObject<HTMLElement>
fromRef: RefObject<HTMLElement>
toRef: RefObject<HTMLElement>
curvature?: number
endYOffset?: number
reverse?: boolean
pathColor?: string
pathWidth?: number
pathOpacity?: number
gradientStartColor?: string
gradientStopColor?: string
delay?: number
duration?: number
className?: string
}
export function AnimatedBeam({
containerRef,
fromRef,
toRef,
curvature = 0,
endYOffset = 0,
reverse = false,
pathColor = "gray",
pathWidth = 2,
pathOpacity = 0.2,
gradientStartColor = "#ffaa40",
gradientStopColor = "#9c40ff",
delay = 0,
duration = Math.random() * 3 + 4,
className,
}: AnimatedBeamProps) {
const [pathD, setPathD] = useState("")
const [svgDimensions, setSvgDimensions] = useState({ width: 0, height: 0 })
const id = `beam-${Math.random().toString(36).substr(2, 9)}`
useEffect(() => {
const updatePath = () => {
if (containerRef.current && fromRef.current && toRef.current) {
const containerRect = containerRef.current.getBoundingClientRect()
const fromRect = fromRef.current.getBoundingClientRect()
const toRect = toRef.current.getBoundingClientRect()
const svgWidth = containerRect.width
const svgHeight = containerRect.height
setSvgDimensions({ width: svgWidth, height: svgHeight })
const startX = fromRect.left - containerRect.left + fromRect.width / 2
const startY = fromRect.top - containerRect.top + fromRect.height / 2
const endX = toRect.left - containerRect.left + toRect.width / 2
const endY =
toRect.top - containerRect.top + toRect.height / 2 + endYOffset
const controlY = startY - curvature
const d = `M ${startX},${startY} Q ${
(startX + endX) / 2
},${controlY} ${endX},${endY}`
setPathD(d)
}
}
updatePath()
window.addEventListener("resize", updatePath)
return () => {
window.removeEventListener("resize", updatePath)
}
}, [containerRef, fromRef, toRef, curvature, endYOffset])
return (
<svg
fill="none"
width={svgDimensions.width}
height={svgDimensions.height}
xmlns="http://www.w3.org/2000/svg"
className={cn(
"pointer-events-none absolute left-0 top-0 transform-gpu stroke-2",
className
)}
>
<path
d={pathD}
stroke={pathColor}
strokeWidth={pathWidth}
strokeOpacity={pathOpacity}
strokeLinecap="round"
/>
<path
d={pathD}
strokeWidth={pathWidth}
stroke={`url(#${id})`}
strokeOpacity="1"
strokeLinecap="round"
/>
<defs>
<motion.linearGradient
id={id}
gradientUnits="userSpaceOnUse"
initial={{
x1: reverse ? "100%" : "0%",
x2: reverse ? "100%" : "0%",
y1: "0%",
y2: "0%",
}}
animate={{
x1: reverse ? "-20%" : "120%",
x2: reverse ? "0%" : "100%",
}}
transition={{
delay,
duration,
ease: [0.16, 1, 0.3, 1],
repeat: Infinity,
repeatDelay: 0,
}}
>
<stop stopColor={gradientStartColor} stopOpacity="0" />
<stop stopColor={gradientStartColor} />
<stop offset="0.325" stopColor={gradientStopColor} />
<stop offset="1" stopColor={gradientStopColor} stopOpacity="0" />
</motion.linearGradient>
</defs>
</svg>
)
}
// Usage - connecting two elements
const containerRef = useRef<HTMLDivElement>(null)
const div1Ref = useRef<HTMLDivElement>(null)
const div2Ref = useRef<HTMLDivElement>(null)
<div ref={containerRef} className="relative">
<div ref={div1Ref} className="...">Element 1</div>
<div ref={div2Ref} className="...">Element 2</div>
<AnimatedBeam
containerRef={containerRef}
fromRef={div1Ref}
toRef={div2Ref}
curvature={-50}
/>
</div>Globe (3D)
// components/magicui/globe.tsx
"use client"
import { useEffect, useRef } from "react"
import createGlobe from "cobe"
import { cn } from "@/lib/utils"
interface GlobeProps {
className?: string
config?: {
width?: number
height?: number
phi?: number
theta?: number
dark?: number
diffuse?: number
mapSamples?: number
mapBrightness?: number
baseColor?: [number, number, number]
markerColor?: [number, number, number]
glowColor?: [number, number, number]
markers?: { location: [number, number]; size: number }[]
}
}
const defaultConfig = {
width: 800,
height: 800,
phi: 0,
theta: 0,
dark: 1,
diffuse: 1.2,
mapSamples: 16000,
mapBrightness: 6,
baseColor: [0.3, 0.3, 0.3] as [number, number, number],
markerColor: [0.1, 0.8, 1] as [number, number, number],
glowColor: [0.1, 0.1, 0.1] as [number, number, number],
markers: [
{ location: [52.52, 13.405], size: 0.05 }, // Berlin
{ location: [40.7128, -74.006], size: 0.07 }, // NYC
{ location: [35.6762, 139.6503], size: 0.06 }, // Tokyo
{ location: [51.5074, -0.1278], size: 0.05 }, // London
],
}
export function Globe({ className, config = {} }: GlobeProps) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const mergedConfig = { ...defaultConfig, ...config }
useEffect(() => {
let phi = mergedConfig.phi
let width = 0
const onResize = () => {
if (canvasRef.current) {
width = canvasRef.current.offsetWidth
}
}
window.addEventListener("resize", onResize)
onResize()
const globe = createGlobe(canvasRef.current!, {
devicePixelRatio: 2,
width: mergedConfig.width * 2,
height: mergedConfig.height * 2,
phi: mergedConfig.phi,
theta: mergedConfig.theta,
dark: mergedConfig.dark,
diffuse: mergedConfig.diffuse,
mapSamples: mergedConfig.mapSamples,
mapBrightness: mergedConfig.mapBrightness,
baseColor: mergedConfig.baseColor,
markerColor: mergedConfig.markerColor,
glowColor: mergedConfig.glowColor,
markers: mergedConfig.markers,
onRender: (state) => {
state.phi = phi
phi += 0.005
},
})
return () => {
globe.destroy()
window.removeEventListener("resize", onResize)
}
}, [])
return (
<canvas
ref={canvasRef}
className={cn("h-full w-full", className)}
style={{
width: mergedConfig.width,
height: mergedConfig.height,
maxWidth: "100%",
aspectRatio: 1,
}}
/>
)
}
// Usage
// npm install cobe
<Globe
className="mx-auto"
config={{
width: 500,
height: 500,
markers: [
{ location: [52.23, 21.01], size: 0.08 }, // Warsaw
],
}}
/>Example landing page
// app/page.tsx
import { ShimmerButton } from "@/components/magicui/shimmer-button"
import { TypingAnimation } from "@/components/magicui/typing-animation"
import { GradientText } from "@/components/magicui/gradient-text"
import { Particles } from "@/components/magicui/particles"
import { BentoGrid, BentoCard } from "@/components/magicui/bento-grid"
import { Globe } from "@/components/magicui/globe"
export default function LandingPage() {
return (
<main className="relative min-h-screen bg-black text-white">
{/* Hero Section */}
<section className="relative h-screen flex items-center justify-center overflow-hidden">
<Particles
className="absolute inset-0"
quantity={100}
color="#ffffff"
/>
<div className="relative z-10 text-center">
<TypingAnimation
text="Build Something Magic"
duration={100}
className="text-6xl font-bold mb-6"
/>
<p className="text-xl text-gray-400 mb-8 max-w-2xl mx-auto">
Create stunning <GradientText>landing pages</GradientText> with
beautiful animations and effects
</p>
<div className="flex gap-4 justify-center">
<ShimmerButton>
Get Started
</ShimmerButton>
<button className="px-6 py-3 border border-white/20 rounded-full hover:bg-white/10 transition">
Learn More
</button>
</div>
</div>
</section>
{/* Features Section */}
<section className="py-24 px-6">
<div className="max-w-6xl mx-auto">
<h2 className="text-4xl font-bold text-center mb-16">
<GradientText>Features</GradientText>
</h2>
<BentoGrid className="lg:grid-rows-3">
<BentoCard
name="Copy & Paste"
className="lg:col-span-2 lg:row-span-2"
description="Simply copy components and customize them"
href="#"
cta="Learn more"
/>
<BentoCard
name="TypeScript"
className="lg:col-span-1"
description="Full TypeScript support"
href="#"
cta="Explore"
/>
<BentoCard
name="Dark Mode"
className="lg:col-span-1"
description="Native dark mode support"
href="#"
cta="See"
/>
<BentoCard
name="Responsive"
className="lg:col-span-2"
description="Works on all screen sizes"
href="#"
cta="Demo"
/>
</BentoGrid>
</div>
</section>
{/* Globe Section */}
<section className="py-24 relative">
<div className="absolute inset-0 flex items-center justify-center">
<Globe className="opacity-50" />
</div>
<div className="relative z-10 text-center">
<h2 className="text-4xl font-bold mb-4">
Used Worldwide
</h2>
<p className="text-gray-400">
Developers around the globe trust Magic UI
</p>
</div>
</section>
{/* CTA Section */}
<section className="py-24 text-center">
<h2 className="text-4xl font-bold mb-6">
Ready to get started?
</h2>
<ShimmerButton
shimmerColor="#ffffff"
background="linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
>
Start Building Today
</ShimmerButton>
</section>
</main>
)
}FAQ - frequently asked questions
Is Magic UI free?
Yes, Magic UI is fully free and open-source under the MIT license. You can use the components in commercial projects without any restrictions.
Do I need to install an npm package?
No, Magic UI uses the copy-paste approach. You copy the component code into your project. You can also use the shadcn CLI to automatically add components.
What are the requirements?
- React 18+
- Tailwind CSS
- Framer Motion (
npm install framer-motion) - clsx and tailwind-merge for the cn() utility function
Does Magic UI work with Next.js?
Yes, Magic UI works perfectly with Next.js (both Pages Router and App Router). Some components require the "use client" directive.
Can I modify the components?
Yes, that is the main advantage of the copy-paste approach. The code is yours - you can modify, extend, and customize it to your needs however you want.
How do I add dark mode?
Magic UI natively supports dark mode through Tailwind dark: classes. Make sure you have darkMode: ["class"] configured in your tailwind.config.js.
Are the components accessible?
Magic UI strives to maintain basic accessibility, but due to the decorative nature of some effects, full WCAG compliance is not guaranteed. Animations can be disabled through prefers-reduced-motion.
How do I report a bug or suggest a new component?
You can open an issue on the Magic UI GitHub repository or submit a Pull Request with a new component.
Summary
Magic UI is a collection of beautiful, animated React components that let you create standout landing pages and applications. Thanks to the copy-paste approach, you have full control over the code, and the integration with Tailwind CSS and Framer Motion provides professional animations without boilerplate.
Key advantages of Magic UI:
- Copy-paste - zero dependencies, full control
- Beautiful animations - shimmer, particles, beams, globe
- TypeScript - complete types
- Tailwind + Framer Motion - familiar technologies
- Dark mode - native support
- shadcn compatible - easy integration
Whether it is for a SaaS landing page, a developer portfolio, or a product page - Magic UI delivers components that make an impression.