Skip to content
Development
Skill

/animation-patterns

Framer Motion patterns, page transitions, skeleton loading, scroll-linked animations, and gesture-based interactions for React.

From plugin
vibecosystem
532200 skills138 agents7 hooks
Install
$ npx -y skills add vibeeval/vibecosystem --skill animation-patterns --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/animation-patterns

Context preview

The summary Claude sees to decide when to auto-load this skill.

Framer Motion patterns, page transitions, skeleton loading, scroll-linked animations, and gesture-based interactions for React.

SKILL.md

animation-patterns.SKILL.md
name: animation-patterns
description: Framer Motion patterns, page transitions, skeleton loading, scroll-linked animations, and gesture-based interactions for React.

Animation Patterns

Framer Motion patterns for production-quality React animations.

Framer Motion Basics (motion components, variants)

// Install: npm install framer-motion

import { motion } from 'framer-motion'

// Basic motion component
<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.3, ease: 'easeOut' }}
/>

// Variants — define states, animate by name
const cardVariants = {
  hidden: { opacity: 0, y: 20, scale: 0.98 },
  visible: {
    opacity: 1,
    y: 0,
    scale: 1,
    transition: { duration: 0.25, ease: [0.16, 1, 0.3, 1] },
  },
  hover: {
    y: -4,
    boxShadow: '0 12px 24px -4px rgb(0 0 0 / 0.15)',
    transition: { duration: 0.2 },
  },
}

function Card({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      variants={cardVariants}
      initial="hidden"
      animate="visible"
      whileHover="hover"
      className="rounded-lg border bg-white p-4"
    >
      {children}
    </motion.div>
  )
}

Page Transitions with AnimatePresence

import { AnimatePresence, motion } from 'framer-motion'
import { usePathname } from 'next/navigation'

const pageVariants = {
  initial: { opacity: 0, x: -8 },
  enter:   { opacity: 1, x: 0, transition: { duration: 0.2, ease: 'easeOut' } },
  exit:    { opacity: 0, x: 8,  transition: { duration: 0.15, ease: 'easeIn' } },
}

// app/layout.tsx (Next.js App Router)
export default function RootLayout({ children }: { children: React.ReactNode }) {
  const pathname = usePathname()

  return (
    <html>
      <body>
        <AnimatePresence mode="wait">
          <motion.main
            key={pathname}
            variants={pageVariants}
            initial="initial"
            animate="enter"
            exit="exit"
          >
            {children}
          </motion.main>
        </AnimatePresence>
      </body>
    </html>
  )
}

// Modal transitions — mount/unmount
function Modal({ isOpen, onClose, children }: ModalProps) {
  return (
    <AnimatePresence>
      {isOpen && (
        <>
          <motion.div
            key="overlay"
            className="fixed inset-0 bg-black/40"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            onClick={onClose}
          />
          <motion.div
            key="modal"
            className="fixed inset-x-4 top-[10%] mx-auto max-w-lg rounded-xl bg-white p-6 shadow-xl"
            initial={{ opacity: 0, scale: 0.96, y: 16 }}
            animate={{ opacity: 1, scale: 1, y: 0 }}
            exit={{ opacity: 0, scale: 0.96, y: 8 }}
            transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
          >
            {children}
          </motion.div>
        </>
      )}
    </AnimatePresence>
  )
}

Layout Animations (Shared Layout, Layout ID)

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

// Tabs with animated indicator
function AnimatedTabs({ tabs }: { tabs: string[] }) {
  const [active, setActive] = useState(tabs[0])

  return (
    <LayoutGroup>
      <div className="flex gap-1 rounded-lg bg-gray-100 p-1">
        {tabs.map(tab => (
          <button
            key={tab}
            onClick={() => setActive(tab)}
            className="relative px-4 py-2 text-sm font-medium"
          >
            {active === tab && (
              <motion.div
                layoutId="tab-indicator"      // shared across tabs
                className="absolute inset-0 rounded-md bg-white shadow-sm"
                transition={{ type: 'spring', stiffness: 400, damping: 30 }}
              />
            )}
            <span className="relative z-10">{tab}</span>
          </button>
        ))}
      </div>
    </LayoutGroup>
  )
}

// Expandable card with layout animation
function ExpandableCard({ title, body }: { title: string; body: string }) {
  const [expanded, setExpanded] = useState(false)

  return (
    <motion.div
      layout                                // animate height changes
      className="rounded-lg border bg-white p-4 cursor-pointer"
      onClick={() => setExpanded(e => !e)}
    >
      <motion.h3 layout="position" className="font-semibold">{title}</motion.h3>
      <AnimatePresence>
        {expanded && (
          <motion.p
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            className="mt-2 text-gray-600 text-sm"
          >
            {body}
          </motion.p>
        )}
      </AnimatePresence>
    </motion.div>
  )
}

Skeleton Loading Components

// Base skeleton primitive
function Skeleton({ className }: { className?: string }) {
  return (
    <div
      className={cn('animate-pulse rounded bg-gray-200 dark:bg-gray-700', className)}
      aria-hidden="true"
    />
  )
}

// Shimmer variant (more polished)
function SkeletonShimmer({ className }: { className?: string }) {
  return (
    <div className={cn('relative overflow-hidden rounded bg-gray-200', className)}>
      <motion.div
        className="absolute inset-0 bg-gradient-to-r from-transparent via-white/60 to-transparent"
        animate={{ x: ['-100%', '100%'] }}
        transition={{ repeat: Infinity, duration: 1.2, ease: 'linear' }}
      />
    </div>
  )
}

// Card skeleton
function CardSkeleton() {
  return (
    <div className="rounded-lg border bg-white p-4 space-y-3" aria-busy="true" aria-label="Loading">
      <div className="flex items-center gap-3">
        <Skeleton className="size-10 rounded-full" />
        <div className="flex-1 space-y-2">
          <Skeleton className="h-4 w-1/2" />
          <Skeleton className="h-3 w-1/3" />
        </div>
      </div>
      <Skeleton className="h-3 w-full
Read more
Ships withvibecosystem

Your AI software team. Built on Claude Code. vibecosystem turns Claude Code into a full AI software team — 138 specialized agents that plan, build, review, test, and learn from every mistake. No configuration needed — just install and code.

Get the whole plugin

Other skills on vibecosystem.