Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds
Torna alle collezioni
Guide45 min read

Magic UI - Kompletny Przewodnik po Animowanych Komponentach React

Magic UI is a collection of beautiful, animated React components built on shadcn/ui and Framer Motion. Visual effects for landing pages, portfolios, and SaaS applications. Copy-paste components.

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

  1. Zero zależności - tylko Framer Motion i Tailwind
  2. Copy-paste - pełna kontrola, zero vendor lock-in
  3. TypeScript - kompletne typy dla wszystkich komponentów
  4. Customizable - kod jest Twój, zmieniaj jak chcesz
  5. Performance - zoptymalizowane animacje
  6. Responsive - działa na wszystkich urządzeniach
  7. Dark mode - natywne wsparcie
  8. Accessible - zgodne z WCAG gdzie możliwe

Magic UI vs Aceternity UI vs Framer Motion

CechaMagic UIAceternity UIFramer Motion
TypKomponentyKomponentyBiblioteka
InstalacjaCopy-pasteCopy-pastenpm install
CustomizePełna kontrolaPełna kontrolaProgramowanie
Komponenty~50+~40+Build your own
Learning curveNiskaNiskaŚrednia
TypeScript
Dark modeN/A
shadcn compatibleN/A
LicencjaMITMITMIT

Instalacja i setup

Wymagania

Code
Bash
# Twój projekt powinien mieć:
- React 18+
- Tailwind CSS
- Framer Motion
- (opcjonalnie) shadcn/ui

# Instalacja zależności
npm install framer-motion clsx tailwind-merge

Konfiguracja Tailwind

JStailwind.config.js
JavaScript
// 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

TSlib/utils.ts
TypeScript
// 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

Code
Bash
# 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].tsx

Komponenty - Buttons

Shimmer Button

TScomponents/magicui/shimmer-button.tsx
TypeScript
// 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

TScomponents/magicui/pulsating-button.tsx
TypeScript
// 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

TScomponents/magicui/animated-subscribe-button.tsx
TypeScript
// 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

TScomponents/magicui/typing-animation.tsx
TypeScript
// 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

TScomponents/magicui/text-reveal.tsx
TypeScript
// 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

TScomponents/magicui/gradient-text.tsx
TypeScript
// 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

TScomponents/magicui/word-rotate.tsx
TypeScript
// 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

TScomponents/magicui/particles.tsx
TypeScript
// 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

TScomponents/magicui/dot-pattern.tsx
TypeScript
// 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

TScomponents/magicui/grid-pattern.tsx
TypeScript
// 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

TScomponents/magicui/meteors.tsx
TypeScript
// 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

TScomponents/magicui/bento-grid.tsx
TypeScript
// 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

TScomponents/magicui/spotlight-card.tsx
TypeScript
// 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

TScomponents/magicui/animated-beam.tsx
TypeScript
// 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)

TScomponents/magicui/globe.tsx
TypeScript
// 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

TSapp/page.tsx
TypeScript
// 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

  1. Zero dependencies - only Framer Motion and Tailwind
  2. Copy-paste - full control, zero vendor lock-in
  3. TypeScript - complete types for all components
  4. Customizable - the code is yours, change it however you want
  5. Performance - optimized animations
  6. Responsive - works on all devices
  7. Dark mode - native support
  8. Accessible - WCAG compliant where possible

Magic UI vs Aceternity UI vs Framer Motion

FeatureMagic UIAceternity UIFramer Motion
TypeComponentsComponentsLibrary
InstallationCopy-pasteCopy-pastenpm install
CustomizeFull controlFull controlProgramming
Components~50+~40+Build your own
Learning curveLowLowMedium
TypeScript
Dark modeN/A
shadcn compatibleN/A
LicenseMITMITMIT

Installation and setup

Requirements

Code
Bash
# Your project should have:
- React 18+
- Tailwind CSS
- Framer Motion
- (optionally) shadcn/ui

# Installing dependencies
npm install framer-motion clsx tailwind-merge

Tailwind configuration

JStailwind.config.js
JavaScript
// 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

TSlib/utils.ts
TypeScript
// 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

Code
Bash
# 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].tsx

Components - buttons

Shimmer Button

TScomponents/magicui/shimmer-button.tsx
TypeScript
// 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

TScomponents/magicui/pulsating-button.tsx
TypeScript
// 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

TScomponents/magicui/animated-subscribe-button.tsx
TypeScript
// 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

TScomponents/magicui/typing-animation.tsx
TypeScript
// 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

TScomponents/magicui/text-reveal.tsx
TypeScript
// 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

TScomponents/magicui/gradient-text.tsx
TypeScript
// 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

TScomponents/magicui/word-rotate.tsx
TypeScript
// 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

TScomponents/magicui/particles.tsx
TypeScript
// 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

TScomponents/magicui/dot-pattern.tsx
TypeScript
// 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

TScomponents/magicui/grid-pattern.tsx
TypeScript
// 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

TScomponents/magicui/meteors.tsx
TypeScript
// 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

TScomponents/magicui/bento-grid.tsx
TypeScript
// 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

TScomponents/magicui/spotlight-card.tsx
TypeScript
// 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

TScomponents/magicui/animated-beam.tsx
TypeScript
// 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)

TScomponents/magicui/globe.tsx
TypeScript
// 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

TSapp/page.tsx
TypeScript
// 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.