İçeriğe geç
react

React Animasyon: Framer Motion Rehberi

Tarık Tunç
React Animasyon: Framer Motion Rehberi

React Animasyon: Framer Motion Rehberi

Animasyonlar, kullanıcı deneyimini zenginleştiren ve arayüzün canlı hissettiren önemli bir unsurdur. Framer Motion, React için en güçlü ve kullanımı en kolay animasyon kütüphanesidir. Deklaratif API'si sayesinde karmaşık animasyonları bile birkaç satır kodla oluşturabilirsiniz. Bu rehberde, Framer Motion'ın temellerinden ileri düzey tekniklere kadar her şeyi ele alacağız.

Neden Framer Motion?

Framer Motion, CSS animasyonlarının ve diğer JS kütüphanelerinin karmaşıklığını ortadan kaldırır. Layout animasyonları, gesture desteği, AnimatePresence ile exit animasyonları ve spring physics gibi özellikleriyle öne çıkar. API tasarımı React'in deklaratif yapısıyla mükemmel uyum sağlar.

Temel Animasyonlar

// npm install framer-motion

import { motion } from 'framer-motion';

// Basit bir fade-in animasyonu
function FadeIn({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.5, ease: 'easeOut' }}
    >
      {children}
    </motion.div>
  );
}

// Hover ve tap animasyonları
function AnimatedButton({ children, onClick }: { children: React.ReactNode; onClick: () => void }) {
  return (
    <motion.button
      onClick={onClick}
      whileHover={{ scale: 1.05, boxShadow: '0 5px 15px rgba(0,0,0,0.2)' }}
      whileTap={{ scale: 0.95 }}
      transition={{ type: 'spring', stiffness: 400, damping: 17 }}
      className="bg-blue-600 text-white px-6 py-3 rounded-lg font-medium"
    >
      {children}
    </motion.button>
  );
}

// Scroll tetiklemeli animasyon
function ScrollReveal({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 50 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true, margin: '-100px' }}
      transition={{ duration: 0.6 }}
    >
      {children}
    </motion.div>
  );
}
İpucu: type: 'spring' transition'ı doğal ve fiziksel animasyonlar oluşturur. stiffness (sertlik) ve damping (sönümleme) değerlerini ayarlayarak animasyonun karakterini belirleyebilirsiniz.

Variants ile Orchestrated Animasyonlar

Variants, birden fazla animasyon durumunu tanımlayıp, parent-child ilişkisiyle otomatik olarak sıralanmasını sağlar. Bu özellik, staggered (kademeli) animasyonlar oluşturmak için idealdir.

import { motion } from 'framer-motion';

const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1, // Her child 0.1s arayla animasyona başlar
      delayChildren: 0.2,
    },
  },
};

const itemVariants = {
  hidden: { opacity: 0, x: -20 },
  visible: {
    opacity: 1,
    x: 0,
    transition: { type: 'spring', stiffness: 100 },
  },
};

interface MenuItem {
  id: string;
  label: string;
  icon: string;
}

function AnimatedMenu({ items }: { items: MenuItem[] }) {
  return (
    <motion.ul
      variants={containerVariants}
      initial="hidden"
      animate="visible"
      className="space-y-2"
    >
      {items.map((item) => (
        <motion.li
          key={item.id}
          variants={itemVariants}
          className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-100 cursor-pointer"
        >
          <span>{item.icon}</span>
          <span>{item.label}</span>
        </motion.li>
      ))}
    </motion.ul>
  );
}

// Kullanım
function Sidebar() {
  const menuItems: MenuItem[] = [
    { id: '1', label: 'Ana Sayfa', icon: '🏠' },
    { id: '2', label: 'Ürünler', icon: '📦' },
    { id: '3', label: 'Siparişler', icon: '📋' },
    { id: '4', label: 'Ayarlar', icon: '⚙️' },
  ];

  return <AnimatedMenu items={menuItems} />;
}

AnimatePresence: Exit Animasyonları

import { motion, AnimatePresence } from 'framer-motion';
import { useState } from 'react';

interface Notification {
  id: string;
  message: string;
  type: 'success' | 'error' | 'info';
}

function NotificationStack() {
  const [notifications, setNotifications] = useState<Notification[]>([]);

  const addNotification = (type: Notification['type'], message: string) => {
    const id = crypto.randomUUID();
    setNotifications((prev) => [...prev, { id, message, type }]);

    // 3 saniye sonra otomatik kaldır
    setTimeout(() => {
      setNotifications((prev) => prev.filter((n) => n.id !== id));
    }, 3000);
  };

  const removeNotification = (id: string) => {
    setNotifications((prev) => prev.filter((n) => n.id !== id));
  };

  const typeColors = {
    success: 'bg-green-500',
    error: 'bg-red-500',
    info: 'bg-blue-500',
  };

  return (
    <div className="fixed top-4 right-4 space-y-2 z-50">
      <AnimatePresence>
        {notifications.map((notification) => (
          <motion.div
            key={notification.id}
            initial={{ opacity: 0, x: 300, scale: 0.8 }}
            animate={{ opacity: 1, x: 0, scale: 1 }}
            exit={{ opacity: 0, x: 300, scale: 0.8 }}
            transition={{ type: 'spring', stiffness: 200, damping: 20 }}
            className={`${typeColors[notification.type]} text-white px-4 py-3 rounded-lg shadow-lg flex items-center gap-3 min-w-[300px]`}
          >
            <span className="flex-1">{notification.message}</span>
            <button onClick={() => removeNotification(notification.id)} className="text-white/80 hover:text-white">
              ✕
            </button>
          </motion.div>
        ))}
      </AnimatePresence>

      {/* Demo butonları */}
      <div className="fixed bottom-4 right-4 space-x-2">
        <button onClick={() => addNotification('success', 'İşlem başarılı!')} className="bg-green-600 text-white px-3 py-1 rounded">Başarı</button>
        <button onClick={() => addNotification('error', 'Bir hata oluştu!')} className="bg-red-600 text-white px-3 py-1 rounded">Hata</button>
      </div>
    </div>
  );
}

Layout Animasyonları

import { motion, LayoutGroup } from 'framer-motion';
import { useState } from 'react';

function FilterableTabs() {
  const [activeTab, setActiveTab] = useState('all');
  const tabs = ['all', 'design', 'development', 'marketing'];

  return (
    <div className="flex gap-2 p-1 bg-gray-100 rounded-lg">
      {tabs.map((tab) => (
        <button
          key={tab}
          onClick={() => setActiveTab(tab)}
          className="relative px-4 py-2 rounded-md text-sm font-medium"
        >
          {activeTab === tab && (
            <motion.div
              layoutId="active-tab"
              className="absolute inset-0 bg-white rounded-md shadow"
              transition={{ type: 'spring', stiffness: 300, damping: 30 }}
            />
          )}
          <span className="relative z-10">{tab}</span>
        </button>
      ))}
    </div>
  );
}
Uyarı: Layout animasyonları performans maliyetli olabilir. Çok sayıda element aynı anda layout animasyonu yapıyorsa, layoutScroll ve layoutDependency prop'larını kullanarak optimizasyon yapın.
Bilgi: Framer Motion, CSS transform ve opacity gibi GPU hızlandırmalı özellikleri kullanır. Bu sayede 60fps animasyonlar sağlanır. Ancak width, height gibi layout tetikleyen özellikler yerine scale kullanmayı tercih edin.

Sonuç

Framer Motion, React uygulamalarınıza profesyonel animasyonlar eklemenin en kolay ve en güçlü yoludur. Basit fade/slide animasyonlarından karmaşık layout geçişlerine, gesture desteğinden exit animasyonlarına kadar geniş bir yelpaze sunar. Variants ile orchestrated animasyonlar, AnimatePresence ile mount/unmount geçişleri ve layoutId ile otomatik layout animasyonları oluşturabilirsiniz. Animasyonları kullanıcı deneyimini iyileştirmek için kullanın, abartmayın.

Kaynaklar

İlgili Yazılar