framer-motion-animations
This skill implements scroll-triggered and entrance animations using Framer Motion, providing consistent, accessible animations with reduced motion support and easy migration from CSS/Intersection Observer solutions.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install sgcarstrends-sgcarstrends-framer-motion-animations
Repository
Skill path: .claude/skills/framer-motion-animations
This skill implements scroll-triggered and entrance animations using Framer Motion, providing consistent, accessible animations with reduced motion support and easy migration from CSS/Intersection Observer solutions.
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: sgcarstrends.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install framer-motion-animations into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/sgcarstrends/sgcarstrends before adding framer-motion-animations to shared team environments
- Use framer-motion-animations for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: framer-motion-animations
description: Create scroll-triggered and entrance animations using Framer Motion (motion package). Use when adding animations, implementing scroll reveals, or migrating from CSS/Intersection Observer.
allowed-tools: Read, Edit, Write, Grep, Glob
---
# Framer Motion Animations Skill
This skill helps you implement consistent, accessible animations across the codebase using Framer Motion (motion package v12+).
## When to Use This Skill
- Adding scroll-triggered reveal animations
- Implementing entrance/exit animations
- Creating staggered list animations
- Migrating from CSS/Intersection Observer to Motion
- Ensuring accessibility with reduced motion support
## Key Files
| File | Purpose |
|------|---------|
| `apps/web/src/app/about/_components/variants.ts` | Shared animation variants |
| `apps/web/src/components/animated-number.tsx` | Number animation component |
## Animation Variants
### Standard Variants (in `variants.ts`)
```typescript
import type { Variants } from "framer-motion";
// Base fade-in-up animation for section content
export const fadeInUpVariants: Variants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.6, ease: [0.16, 1, 0.3, 1] },
},
};
// Container variant for staggered children
export const staggerContainerVariants: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.1, delayChildren: 0.1 },
},
};
// Individual stagger item
export const staggerItemVariants: Variants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5, ease: [0.4, 0, 0.2, 1] },
},
};
// Hero entrance animation (faster, more dramatic)
export const heroEntranceVariants: Variants = {
hidden: { opacity: 0, y: 24, scale: 0.98 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] },
},
};
```
## Usage Patterns
### 1. Scroll-Triggered Animation (Single Element)
```typescript
import { motion, useReducedMotion } from "framer-motion";
import { fadeInUpVariants } from "./variants";
export const Section = () => {
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
variants={shouldReduceMotion ? undefined : fadeInUpVariants}
initial={shouldReduceMotion ? undefined : "hidden"}
whileInView="visible"
viewport={{ once: true, amount: 0.2 }}
>
{/* Content */}
</motion.div>
);
};
```
### 2. Staggered List Animation
```typescript
import { motion, useReducedMotion } from "framer-motion";
import { staggerContainerVariants, staggerItemVariants } from "./variants";
export const FeatureGrid = () => {
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
className="grid grid-cols-2 gap-6"
variants={shouldReduceMotion ? undefined : staggerContainerVariants}
initial={shouldReduceMotion ? undefined : "hidden"}
whileInView="visible"
viewport={{ once: true, amount: 0.2 }}
>
{features.map((feature) => (
<motion.div
key={feature.id}
variants={shouldReduceMotion ? undefined : staggerItemVariants}
>
{/* Feature card */}
</motion.div>
))}
</motion.div>
);
};
```
### 3. Entrance Animation (No Scroll Trigger)
```typescript
import { motion, useReducedMotion } from "framer-motion";
export const HeroSection = () => {
const shouldReduceMotion = useReducedMotion();
// Custom entrance transition with delay
const entranceTransition = (delay: number) =>
shouldReduceMotion
? undefined
: { duration: 1, delay, ease: [0.16, 1, 0.3, 1] as const };
return (
<motion.h1
initial={shouldReduceMotion ? undefined : { opacity: 0, y: 32 }}
animate={{ opacity: 1, y: 0 }}
transition={entranceTransition(0.15)}
>
Headline
</motion.h1>
);
};
```
### 4. Index-Based Stagger (Manual Delays)
```typescript
import { motion, useReducedMotion } from "framer-motion";
interface TimelineItemProps {
index: number;
}
const TimelineItem = ({ index }: TimelineItemProps) => {
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
initial={shouldReduceMotion ? undefined : { opacity: 0, y: 32 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.3 }}
transition={
shouldReduceMotion
? undefined
: { duration: 0.5, delay: index * 0.15, ease: [0.4, 0, 0.2, 1] }
}
>
{/* Content */}
</motion.div>
);
};
```
### 5. Number Animation with Viewport Trigger
```typescript
import { motion, useReducedMotion } from "framer-motion";
import { AnimatedNumber } from "@web/components/animated-number";
import { useState } from "react";
import { staggerItemVariants } from "./variants";
const StatItem = ({ value }: { value: number }) => {
const shouldReduceMotion = useReducedMotion();
const [isInView, setIsInView] = useState(false);
return (
<motion.div
variants={shouldReduceMotion ? undefined : staggerItemVariants}
onViewportEnter={() => setIsInView(true)}
viewport={{ once: true }}
>
{isInView ? <AnimatedNumber value={value} /> : <span>0</span>}
</motion.div>
);
};
```
## Accessibility
**Always use `useReducedMotion()`** to respect user preferences:
```typescript
import { useReducedMotion } from "framer-motion";
const Component = () => {
const shouldReduceMotion = useReducedMotion();
// When reduced motion is preferred:
// - Pass undefined to variants prop
// - Pass undefined to initial prop
// - Pass undefined to transition prop
// - Skip CSS keyframe animations
};
```
## When to Use CSS vs Motion
| Use Case | Recommendation |
|----------|----------------|
| Scroll-triggered reveals | Motion (`whileInView`) |
| Entrance animations | Motion (`initial`/`animate`) |
| Staggered lists | Motion (`staggerChildren`) |
| Hover states | CSS (Tailwind `transition-*`) |
| Infinite loops (background effects) | CSS keyframes |
| Simple state transitions | CSS (`transition-all`) |
## Easing Functions
Standard easing curves used in the codebase:
| Name | Value | Use Case |
|------|-------|----------|
| Smooth decelerate | `[0.16, 1, 0.3, 1]` | Hero entrances, dramatic reveals |
| Standard ease | `[0.4, 0, 0.2, 1]` | General purpose animations |
## Viewport Options
```typescript
viewport={{
once: true, // Only animate once (recommended)
amount: 0.2, // Trigger when 20% visible
// amount: 0.3, // Trigger when 30% visible (for smaller items)
}}
```
## File Organization
**Variants file naming**: Use `variants.ts` (industry standard)
```
src/app/about/
├── _components/
│ ├── variants.ts # Shared animation variants
│ ├── hero-section.tsx # Uses variants
│ ├── stats-section.tsx # Uses variants + AnimatedNumber
│ └── timeline-section.tsx # Uses variants
```
## Migration from CSS/Intersection Observer
When migrating existing animations:
1. Remove `useState(isVisible)` + `useEffect` with `IntersectionObserver`
2. Remove `ref` passed to section element
3. Add `motion.div` with `whileInView`
4. Add `useReducedMotion()` check
5. Import variants from shared file
**Before (Intersection Observer):**
```typescript
const [isVisible, setIsVisible] = useState(false);
const sectionRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => { if (entry.isIntersecting) setIsVisible(true); },
{ threshold: 0.2 }
);
if (sectionRef.current) observer.observe(sectionRef.current);
return () => observer.disconnect();
}, []);
return (
<div ref={sectionRef} className={isVisible ? "animate-fade-in" : "opacity-0"}>
```
**After (Motion):**
```typescript
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
variants={shouldReduceMotion ? undefined : fadeInUpVariants}
initial={shouldReduceMotion ? undefined : "hidden"}
whileInView="visible"
viewport={{ once: true, amount: 0.2 }}
>
```
## Validation Checklist
When implementing animations:
- [ ] Uses `useReducedMotion()` for accessibility
- [ ] Passes `undefined` to variants/initial/transition when reduced motion preferred
- [ ] Uses shared variants from `variants.ts` where applicable
- [ ] Uses `viewport={{ once: true }}` for scroll-triggered animations
- [ ] Keeps hover states as CSS transitions (not Motion)
- [ ] Uses CSS keyframes for infinite/background animations
## Related Files
- `apps/web/CLAUDE.md` - Web app conventions
- `apps/web/src/app/about/_components/variants.ts` - Animation variants
- `apps/web/src/components/animated-number.tsx` - Number animation component