Animates SVG paths with Framer Motion pathLength and CSS stroke-dasharray, morphs shapes, and creates vector effects for icons, logos, and data visualizations in React/TSX files.
How this skill is triggered — by the user, by Claude, or both
Slash command
/react-animation-studio:svg-animationsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Expert knowledge for SVG path animations, morphing shapes, and vector graphics effects that create stunning visual experiences with resolution-independent graphics.
Expert knowledge for SVG path animations, morphing shapes, and vector graphics effects that create stunning visual experiences with resolution-independent graphics.
Activate this skill when:
**/*.svg files**/*.tsx containing SVG elements**/icons/*.tsx with animated icons**/components/**/Animated*.tsx with SVGpathLength, stroke-dasharrayimport { motion } from 'framer-motion';
function DrawPath() {
return (
<svg viewBox="0 0 100 100" className="w-24 h-24">
<motion.path
d="M10 50 Q 50 10, 90 50 T 90 90"
fill="transparent"
stroke="#3B82F6"
strokeWidth="4"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 2, ease: 'easeInOut' }}
/>
</svg>
);
}
.svg-path {
stroke-dasharray: 1000; /* Total path length */
stroke-dashoffset: 1000; /* Start hidden */
animation: draw 2s ease-in-out forwards;
}
@keyframes draw {
to {
stroke-dashoffset: 0;
}
}
// Get actual path length for accurate animation
useEffect(() => {
const path = document.querySelector('.animated-path') as SVGPathElement;
if (path) {
const length = path.getTotalLength();
console.log('Path length:', length);
}
}, []);
import { motion } from 'framer-motion';
function AnimatedCheck({ isVisible }: { isVisible: boolean }) {
return (
<svg viewBox="0 0 24 24" className="w-6 h-6">
{/* Circle background */}
<motion.circle
cx="12"
cy="12"
r="10"
fill="#22C55E"
initial={{ scale: 0 }}
animate={{ scale: isVisible ? 1 : 0 }}
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
/>
{/* Checkmark path */}
<motion.path
d="M6 12l4 4 8-8"
fill="transparent"
stroke="white"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
initial={{ pathLength: 0 }}
animate={{ pathLength: isVisible ? 1 : 0 }}
transition={{ delay: 0.2, duration: 0.3, ease: 'easeOut' }}
/>
</svg>
);
}
import { motion } from 'framer-motion';
function MenuIcon({ isOpen }: { isOpen: boolean }) {
const variant = isOpen ? 'open' : 'closed';
const topLineVariants = {
closed: { rotate: 0, y: 0 },
open: { rotate: 45, y: 8 },
};
const middleLineVariants = {
closed: { opacity: 1 },
open: { opacity: 0 },
};
const bottomLineVariants = {
closed: { rotate: 0, y: 0 },
open: { rotate: -45, y: -8 },
};
return (
<svg viewBox="0 0 24 24" className="w-6 h-6">
<motion.line
x1="4" y1="6" x2="20" y2="6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
variants={topLineVariants}
animate={variant}
transition={{ duration: 0.3 }}
style={{ transformOrigin: 'center' }}
/>
<motion.line
x1="4" y1="12" x2="20" y2="12"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
variants={middleLineVariants}
animate={variant}
transition={{ duration: 0.2 }}
/>
<motion.line
x1="4" y1="18" x2="20" y2="18"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
variants={bottomLineVariants}
animate={variant}
transition={{ duration: 0.3 }}
style={{ transformOrigin: 'center' }}
/>
</svg>
);
}
import { motion } from 'framer-motion';
function Spinner() {
return (
<svg viewBox="0 0 50 50" className="w-12 h-12">
{/* Background circle */}
<circle
cx="25"
cy="25"
r="20"
fill="none"
stroke="#E5E7EB"
strokeWidth="4"
/>
{/* Animated arc */}
<motion.circle
cx="25"
cy="25"
r="20"
fill="none"
stroke="#3B82F6"
strokeWidth="4"
strokeLinecap="round"
initial={{ pathLength: 0, rotate: 0 }}
animate={{
pathLength: [0, 0.5, 0],
rotate: 360,
}}
transition={{
pathLength: {
duration: 1.5,
repeat: Infinity,
ease: 'easeInOut',
},
rotate: {
duration: 1.5,
repeat: Infinity,
ease: 'linear',
},
}}
style={{ transformOrigin: 'center' }}
/>
</svg>
);
}
import { motion, useAnimation } from 'framer-motion';
const shapes = {
circle: 'M50,10 A40,40 0 1,1 50,90 A40,40 0 1,1 50,10',
square: 'M10,10 L90,10 L90,90 L10,90 Z',
triangle: 'M50,10 L90,90 L10,90 Z',
};
function MorphingShape() {
const [currentShape, setCurrentShape] = useState<keyof typeof shapes>('circle');
return (
<svg viewBox="0 0 100 100" className="w-32 h-32">
<motion.path
fill="#3B82F6"
animate={{ d: shapes[currentShape] }}
transition={{ duration: 0.5, ease: 'easeInOut' }}
/>
</svg>
);
}
import gsap from 'gsap';
import { MorphSVGPlugin } from 'gsap/MorphSVGPlugin';
gsap.registerPlugin(MorphSVGPlugin);
function GSAPMorph() {
useEffect(() => {
gsap.to('#shape', {
duration: 1,
morphSVG: '#targetShape',
ease: 'power2.inOut',
});
}, []);
return (
<svg>
<path id="shape" d="M10,10 L90,10 L90,90 L10,90 Z" />
<path id="targetShape" d="M50,10 A40,40 0 1,1 50,90 A40,40 0 1,1 50,10" style={{ visibility: 'hidden' }} />
</svg>
);
}
function AnimatedLogo() {
const controls = useAnimation();
const logoVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.2,
},
},
};
const pathVariants = {
hidden: { pathLength: 0, opacity: 0 },
visible: {
pathLength: 1,
opacity: 1,
transition: { duration: 1, ease: 'easeInOut' },
},
};
const fillVariants = {
hidden: { fillOpacity: 0 },
visible: {
fillOpacity: 1,
transition: { delay: 0.5, duration: 0.5 },
},
};
return (
<motion.svg
viewBox="0 0 200 60"
className="w-48"
variants={logoVariants}
initial="hidden"
animate="visible"
>
{/* Letter outlines */}
<motion.path
d="M10,50 L10,10 L40,10 L40,30 L20,30 L20,50"
fill="none"
stroke="#3B82F6"
strokeWidth="2"
variants={pathVariants}
/>
{/* Fill after stroke */}
<motion.path
d="M10,50 L10,10 L40,10 L40,30 L20,30 L20,50"
fill="#3B82F6"
variants={fillVariants}
/>
</motion.svg>
);
}
function AnimatedBarChart({ data }: { data: number[] }) {
const maxValue = Math.max(...data);
return (
<svg viewBox="0 0 300 200" className="w-full h-48">
{data.map((value, index) => {
const barHeight = (value / maxValue) * 160;
const barWidth = 40;
const gap = 20;
const x = index * (barWidth + gap) + 20;
const y = 180 - barHeight;
return (
<motion.rect
key={index}
x={x}
y={180}
width={barWidth}
height={0}
fill="#3B82F6"
rx="4"
initial={{ height: 0, y: 180 }}
animate={{ height: barHeight, y }}
transition={{
duration: 0.8,
delay: index * 0.1,
ease: [0.25, 0.1, 0.25, 1],
}}
/>
);
})}
</svg>
);
}
function CircleProgress({ progress }: { progress: number }) {
const radius = 40;
const circumference = 2 * Math.PI * radius;
return (
<svg viewBox="0 0 100 100" className="w-24 h-24 -rotate-90">
{/* Background circle */}
<circle
cx="50"
cy="50"
r={radius}
fill="none"
stroke="#E5E7EB"
strokeWidth="8"
/>
{/* Progress circle */}
<motion.circle
cx="50"
cy="50"
r={radius}
fill="none"
stroke="#3B82F6"
strokeWidth="8"
strokeLinecap="round"
initial={{ strokeDasharray: circumference, strokeDashoffset: circumference }}
animate={{ strokeDashoffset: circumference * (1 - progress) }}
transition={{ duration: 1, ease: 'easeOut' }}
/>
{/* Percentage text */}
<motion.text
x="50"
y="50"
textAnchor="middle"
dominantBaseline="middle"
className="text-2xl font-bold fill-current rotate-90"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
{Math.round(progress * 100)}%
</motion.text>
</svg>
);
}
will-change: transform for GPU accelerationpathLength property for easy animations<g> for batch transformsCreated by Brookside BI as part of React Animation Studio
npx claudepluginhub thelobbi/claude --plugin react-animation-studioImplements SVG icon, logo, illustration, and path stroke animations using CSS, Framer Motion, or GSAP. Covers motion accessibility with prefers-reduced-motion and fallback behavior.
Animates DOM elements, CSS properties, SVGs, and JavaScript objects using Anime.js timelines, stagger effects, path morphing, and keyframe sequences for complex web animations.
Generates Lottie animations from SVGs, replacing After Effects for motion graphics. Supports bezier curves, morphing, character rigs, and frame-by-frame animation.