Animation
loopwind provides Tailwind-style animation classes that work with time to create smooth video animations without writing custom code.
Note: Animation classes only work with video templates and GIFs. For static images, animations will have no effect since there’s no time context.
Quick Start
export default function MyVideo({ tw, title, subtitle }) {
return (
<div style={tw('flex flex-col items-center justify-center w-full h-full bg-black')}>
{/* Bounce in from below: starts at 0, lasts 400ms */}
<h1 style={tw('text-8xl font-bold text-white ease-out enter-bounce-in-up/0/400')}>
{title}
</h1>
{/* Fade in with upward motion: starts at 300ms, lasts 400ms */}
<p style={tw('text-2xl text-white/80 mt-4 ease-out enter-fade-in-up/300/400')}>
{subtitle}
</p>
{/* Continuous floating animation: repeats every 1s (1000ms) */}
<div style={tw('mt-8 text-4xl loop-float/1000')}>
⬇️
</div>
</div>
);
}
Animation Format
loopwind uses three types of animations with millisecond timing:
| Type | Format | Description |
|---|---|---|
| Enter | enter-{type}/{start}/{duration} | Animations that play when entering |
| Exit | exit-{type}/{start}/{duration} | Animations that play when exiting |
| Loop | loop-{type}/{duration} | Continuous looping animations |
All timing values are in milliseconds (1000ms = 1 second).
Utility-Based Animations
In addition to predefined animations, loopwind supports Tailwind utility-based animations that let you animate any transform or opacity property directly:
// Slide in 20px from the left
<div style={tw('enter-translate-x-5/0/1000')}>Content</div>
// Rotate 90 degrees on entrance
<div style={tw('enter-rotate-90/0/500')}>Spinning</div>
// Fade to 50% opacity in a loop
<div style={tw('loop-opacity-50/1000')}>Pulsing</div>
// Scale down with negative value
<div style={tw('enter--scale-50/0/800')}>Shrinking</div>
Supported Utilities
| Utility | Format | Description | Example |
|---|---|---|---|
| translate-x | enter-translate-x-{value} | Translate horizontally | enter-translate-x-5 = 20pxenter-translate-x-full = 100%enter-translate-x-[20px] = 20px |
| translate-y | enter-translate-y-{value} | Translate vertically | loop-translate-y-10 = 40pxenter-translate-y-1/2 = 50%enter-translate-y-[5rem] = 80px |
| opacity | enter-opacity-{n} | Set opacity (0-100) | enter-opacity-50 = 50% |
| scale | enter-scale-{n} | Scale element (0-200) | enter-scale-100 = 1.0x |
| rotate | enter-rotate-{n} | Rotate in degrees | enter-rotate-45 = 45° |
| skew-x | enter-skew-x-{n} | Skew on X axis in degrees | enter-skew-x-12 = 12° |
| skew-y | enter-skew-y-{n} | Skew on Y axis in degrees | exit-skew-y-6 = 6° |
Translate value formats:
- Numeric:
5= 20px (Tailwind spacing scale: 1 unit = 4px) - Keywords:
full= 100% - Fractions:
1/2= 50%,1/3= 33.333%,2/3= 66.666%, etc. - Arbitrary values:
[20px],[5rem],[10%](rem converts to px: 1rem = 16px)
All utilities work with:
- All prefixes:
enter-,exit-,loop-,animate- - Negative values: Prefix with
-(e.g.,-translate-x-5,-rotate-45) - Timing syntax: Add
/start/duration(e.g.,enter-translate-x-5/0/800)
Translate Animations
// Numeric (Tailwind spacing): 20px (5 * 4px)
<div style={tw('enter-translate-x-5/0/500')}>Content</div>
// Keyword: Full width (100%)
<div style={tw('enter-translate-y-full/0/800')}>Dropping full height</div>
// Fraction: Half width (50%)
<div style={tw('enter-translate-x-1/2/0/600')}>Slide in halfway</div>
// Arbitrary values: Exact px or rem
<div style={tw('enter-translate-y-[20px]/0/500')}>Slide 20px</div>
<div style={tw('enter-translate-x-[5rem]/0/800')}>Slide 5rem (80px)</div>
// Loop with fractions
<div style={tw('loop-translate-y-1/4/1000')}>Oscillate 25%</div>
// Negative values
<div style={tw('exit--translate-y-8/2000/500')}>Rising</div>
Opacity Animations
// Fade to 100% opacity
<div style={tw('enter-opacity-100/0/500')}>Fading In</div>
// Fade to 50% opacity
<div style={tw('enter-opacity-50/0/800')}>Half Opacity</div>
// Pulse between 50% and 100%
<div style={tw('loop-opacity-50/1000')}>Pulsing</div>
// Fade out to 0%
<div style={tw('exit-opacity-0/2500/500')}>Vanishing</div>
Scale Animations
// Scale from 0 to 100% (1.0x)
<div style={tw('enter-scale-100/0/500')}>Growing</div>
// Scale to 150% (1.5x)
<div style={tw('enter-scale-150/0/800')}>Enlarging</div>
// Pulse scale in a loop
<div style={tw('loop-scale-110/1000')}>Breathing</div>
// Scale down to 50%
<div style={tw('exit-scale-50/2000/500')}>Shrinking</div>
Rotate Animations
// Rotate 90 degrees
<div style={tw('enter-rotate-90/0/500')}>Quarter Turn</div>
// Rotate 180 degrees
<div style={tw('enter-rotate-180/0/1000')}>Half Turn</div>
// Continuous rotation in loop (360 degrees per cycle)
<div style={tw('loop-rotate-360/2000')}>Spinning</div>
// Rotate backwards with negative value
<div style={tw('enter--rotate-45/0/500')}>Counter Rotation</div>
Skew Animations
// Skew on X axis
<div style={tw('enter-skew-x-12/0/500')}>Slanted</div>
// Skew on Y axis
<div style={tw('enter-skew-y-6/0/800')}>Tilted</div>
// Oscillating skew in loop
<div style={tw('loop-skew-x-6/1000')}>Wobbling</div>
// Negative skew
<div style={tw('exit--skew-x-12/2000/500')}>Reverse Slant</div>
Combining Utilities
You can combine multiple utility animations on the same element:
// Translate and rotate together
<div style={tw('enter-translate-y-10/0/500 enter-rotate-45/0/500')}>
Flying In
</div>
// Fade and scale
<div style={tw('enter-opacity-100/0/800 enter-scale-100/0/800')}>
Appearing
</div>
// Enter with translate, exit with rotation
<div style={tw('enter-translate-x-5/0/500 exit-rotate-180/2500/500')}>
Slide and Spin
</div>
Bracket Notation
For more CSS-like syntax, you can use brackets with units:
// Using bracket notation with seconds
<h1 style={tw('enter-slide-up/[0.6s]/[1.5s]')}>Hello</h1>
// Using bracket notation with milliseconds
<h1 style={tw('enter-fade-in/[300ms]/[800ms]')}>World</h1>
// Mix and match - plain numbers are milliseconds
<h1 style={tw('enter-bounce-in/0/[1.2s]')}>Mixed</h1>
Enter Animations
Format: enter-{type}/{startMs}/{durationMs}
startMs- when the animation begins (milliseconds from start)durationMs- how long the animation lasts
When values are omitted (enter-fade-in), it uses the full video duration.
Fade Animations
Simple opacity transitions with optional direction.
// Fade in from 0ms to 500ms
<h1 style={tw('enter-fade-in/0/500')}>Hello</h1>
// Fade in with upward motion
<h1 style={tw('enter-fade-in-up/0/600')}>Hello</h1>
| Class | Description |
|---|---|
enter-fade-in/0/500 | Fade in (opacity 0 → 1) |
enter-fade-in-up/0/500 | Fade in + slide up (30px) |
enter-fade-in-down/0/500 | Fade in + slide down (30px) |
enter-fade-in-left/0/500 | Fade in + slide from left (30px) |
enter-fade-in-right/0/500 | Fade in + slide from right (30px) |
Slide Animations
Larger movement (100px) with fade.
// Slide in from left: starts at 0, lasts 500ms
<div style={tw('enter-slide-left/0/500')}>Content</div>
// Slide up from bottom: starts at 200ms, lasts 600ms
<div style={tw('enter-slide-up/200/600')}>Content</div>
| Class | Description |
|---|---|
enter-slide-left/0/500 | Slide in from left (100px) |
enter-slide-right/0/500 | Slide in from right (100px) |
enter-slide-up/0/500 | Slide in from bottom (100px) |
enter-slide-down/0/500 | Slide in from top (100px) |
Bounce Animations
Playful entrance with overshoot effect.
// Bounce in with scale overshoot
<h1 style={tw('enter-bounce-in/0/500')}>Bouncy!</h1>
// Bounce in from below
<div style={tw('enter-bounce-in-up/0/600')}>Pop!</div>
| Class | Description |
|---|---|
enter-bounce-in/0/500 | Bounce in with scale overshoot |
enter-bounce-in-up/0/500 | Bounce in from below |
enter-bounce-in-down/0/500 | Bounce in from above |
enter-bounce-in-left/0/500 | Bounce in from left |
enter-bounce-in-right/0/500 | Bounce in from right |
Scale & Zoom Animations
Size-based transitions.
// Scale in from 50%
<div style={tw('enter-scale-in/0/500')}>Growing</div>
// Zoom in from 0%
<div style={tw('enter-zoom-in/0/1000')}>Zooming</div>
| Class | Description |
|---|---|
enter-scale-in/0/500 | Scale up from 50% to 100% |
enter-zoom-in/0/500 | Zoom in from 0% to 100% |
Rotate & Flip Animations
Rotation-based transitions.
// Rotate in 180 degrees
<div style={tw('enter-rotate-in/0/500')}>Spinning</div>
// 3D flip on X axis
<div style={tw('enter-flip-in-x/0/500')}>Flipping</div>
| Class | Description |
|---|---|
enter-rotate-in/0/500 | Rotate in from -180° |
enter-flip-in-x/0/500 | 3D flip on horizontal axis |
enter-flip-in-y/0/500 | 3D flip on vertical axis |
Exit Animations
Format: exit-{type}/{startMs}/{durationMs}
startMs- when the exit animation beginsdurationMs- how long the exit animation lasts
Exit animations use the same timing system but animate elements out.
// Fade out starting at 2500ms, lasting 500ms (ends at 3000ms)
<h1 style={tw('exit-fade-out/2500/500')}>Goodbye</h1>
// Combined enter and exit on same element
<h1 style={tw('enter-fade-in/0/500 exit-fade-out/2500/500')}>
Hello and Goodbye
</h1>
| Class | Description |
|---|---|
exit-fade-out/2500/500 | Fade out (opacity 1 → 0) |
exit-fade-out-up/2500/500 | Fade out + slide up |
exit-fade-out-down/2500/500 | Fade out + slide down |
exit-fade-out-left/2500/500 | Fade out + slide left |
exit-fade-out-right/2500/500 | Fade out + slide right |
exit-slide-up/2500/500 | Slide out upward (100px) |
exit-slide-down/2500/500 | Slide out downward (100px) |
exit-slide-left/2500/500 | Slide out to left (100px) |
exit-slide-right/2500/500 | Slide out to right (100px) |
exit-scale-out/2500/500 | Scale out to 150% |
exit-zoom-out/2500/500 | Zoom out to 200% |
exit-rotate-out/2500/500 | Rotate out to 180° |
exit-bounce-out/2500/500 | Bounce out with scale |
exit-bounce-out-up/2500/500 | Bounce out upward |
exit-bounce-out-down/2500/500 | Bounce out downward |
exit-bounce-out-left/2500/500 | Bounce out to left |
exit-bounce-out-right/2500/500 | Bounce out to right |
Loop Animations
Format: loop-{type}/{durationMs}
Loop animations repeat every {durationMs} milliseconds:
/1000= 1 second loop/500= 0.5 second loop/2000= 2 second loop
When duration is omitted (loop-bounce), it defaults to 1000ms (1 second).
// Pulse opacity every 500ms
<div style={tw('loop-fade/500')}>Pulsing</div>
// Bounce every 800ms
<div style={tw('loop-bounce/800')}>Bouncing</div>
// Full rotation every 2000ms
<div style={tw('loop-spin/2000')}>Spinning</div>
| Class | Description |
|---|---|
loop-fade/{ms} | Opacity pulse (0.5 → 1 → 0.5) |
loop-bounce/{ms} | Bounce up and down |
loop-spin/{ms} | Full 360° rotation |
loop-ping/{ms} | Scale up + fade out (radar effect) |
loop-wiggle/{ms} | Side to side wiggle |
loop-float/{ms} | Gentle up and down floating |
loop-pulse/{ms} | Scale pulse (1.0 → 1.05 → 1.0) |
loop-shake/{ms} | Shake side to side |
Easing Functions
Add an easing class before the animation class to control the timing curve.
// Ease in (accelerate)
<h1 style={tw('ease-in enter-fade-in/0/1000')}>Accelerating</h1>
// Ease out (decelerate) - default
<h1 style={tw('ease-out enter-fade-in/0/1000')}>Decelerating</h1>
// Ease in-out (smooth)
<h1 style={tw('ease-in-out enter-fade-in/0/1000')}>Smooth</h1>
// Strong cubic easing
<h1 style={tw('ease-out-cubic enter-bounce-in/0/500')}>Dramatic</h1>
| Class | Description | Best For |
|---|---|---|
linear | Constant speed | Mechanical motion |
ease-in | Slow start, fast end | Exit animations |
ease-out | Fast start, slow end (default) | Enter animations |
ease-in-out | Slow start and end | Subtle transitions |
ease-in-cubic | Strong slow start | Dramatic exits |
ease-out-cubic | Strong fast start | Impactful entrances |
ease-in-out-cubic | Strong both ends | Emphasis animations |
ease-in-quart | Very strong slow start | Powerful exits |
ease-out-quart | Very strong fast start | Punchy entrances |
ease-in-out-quart | Very strong both ends | Maximum drama |
Per-Animation-Type Easing
You can apply different easing functions to enter, exit, and loop animations on the same element using enter-ease-*, exit-ease-*, and loop-ease-* classes.
// Different easing for enter and exit
<h1 style={tw('enter-ease-out-cubic enter-fade-in/0/500 exit-ease-in exit-fade-out/2500/500')}>
Smooth entrance, sharp exit
</h1>
// Loop with linear easing, enter with bounce
<div style={tw('enter-ease-out enter-bounce-in/0/400 loop-ease-linear loop-fade/1000')}>
Bouncy entrance, linear loop
</div>
// Default easing still works (applies to all animations)
<div style={tw('ease-in-out enter-fade-in/0/500 exit-fade-out/2500/500')}>
Same easing for both
</div>
// Mix default with specific overrides
<div style={tw('ease-out enter-fade-in/0/500 exit-ease-in-cubic exit-fade-out/2500/500')}>
Default ease-out for enter, cubic-in for exit
</div>
How it works:
- Default easing (
ease-*) applies to ALL animations if no specific override is set - Specific easing (
enter-ease-*,exit-ease-*,loop-ease-*) overrides the default for that animation type - If both are present, specific easing takes priority for its animation type
Available easing classes:
| Default (all animations) | Enter only | Exit only | Loop only |
|---|---|---|---|
ease-in | enter-ease-in | exit-ease-in | loop-ease-in |
ease-out | enter-ease-out | exit-ease-out | loop-ease-out |
ease-in-out | enter-ease-in-out | exit-ease-in-out | loop-ease-in-out |
ease-in-cubic | enter-ease-in-cubic | exit-ease-in-cubic | loop-ease-in-cubic |
ease-out-cubic | enter-ease-out-cubic | exit-ease-out-cubic | loop-ease-out-cubic |
ease-in-out-cubic | enter-ease-in-out-cubic | exit-ease-in-out-cubic | loop-ease-in-out-cubic |
ease-in-quart | enter-ease-in-quart | exit-ease-in-quart | loop-ease-in-quart |
ease-out-quart | enter-ease-out-quart | exit-ease-out-quart | loop-ease-out-quart |
ease-in-out-quart | enter-ease-in-out-quart | exit-ease-in-out-quart | loop-ease-in-out-quart |
linear | enter-ease-linear | exit-ease-linear | loop-ease-linear |
ease-spring | enter-ease-spring | exit-ease-spring | loop-ease-spring |
Spring Easing
Spring easing creates natural, physics-based bouncy animations. Use the built-in ease-spring easing or create custom springs with configurable parameters.
// Default spring easing
<h1 style={tw('ease-spring enter-bounce-in/0/500')}>Bouncy spring!</h1>
// Per-animation-type spring
<div style={tw('enter-ease-spring enter-fade-in/0/500 exit-ease-out exit-fade-out/2500/500')}>
Spring entrance, smooth exit
</div>
// Custom spring with parameters: ease-spring/mass/stiffness/damping
<h1 style={tw('ease-spring/1/100/10 enter-scale-in/0/800')}>
Custom spring (mass=1, stiffness=100, damping=10)
</h1>
// More bouncy spring (lower damping)
<div style={tw('ease-spring/1/170/8 enter-bounce-in-up/0/600')}>
Extra bouncy!
</div>
// Stiffer spring (higher stiffness, faster)
<div style={tw('ease-spring/1/200/12 enter-fade-in-up/0/400')}>
Snappy spring
</div>
// Per-animation-type custom springs
<div style={tw('enter-ease-spring/1/150/10 enter-fade-in/0/500 exit-ease-spring/1/100/15 exit-fade-out/2500/500')}>
Different springs for enter and exit
</div>
Spring parameters:
| Parameter | Description | Effect when increased | Default |
|---|---|---|---|
| mass | Mass of the spring | Slower, more inertia | 1 |
| stiffness | Spring stiffness | Faster, snappier | 100 |
| damping | Damping coefficient | Less bounce, smoother | 10 |
Common spring presets:
// Gentle bounce (default)
ease-spring/1/100/10
// Extra bouncy
ease-spring/1/170/8
// Snappy (no bounce)
ease-spring/1/200/15
// Slow and bouncy
ease-spring/2/100/8
// Fast and tight
ease-spring/0.5/300/20
How spring works:
- Default
ease-spring- Uses a pre-calculated spring curve optimized for most use cases - Custom
ease-spring/mass/stiffness/damping- Generates a physics-based spring curve using the damped harmonic oscillator formula - The spring automatically calculates its ideal duration to reach the final state
- Works with all animation types:
ease-spring,enter-ease-spring,exit-ease-spring,loop-ease-spring
Combining Enter and Exit
You can use both enter and exit animations on the same element:
export default function EnterExit({ tw, title }) {
return (
<div style={tw('flex items-center justify-center w-full h-full bg-black')}>
{/* Fade in during first 500ms, fade out during last 500ms (assuming 3s video) */}
<h1 style={tw('text-8xl font-bold text-white enter-fade-in/0/500 exit-fade-out/2500/500')}>
{title}
</h1>
</div>
);
}
The opacities from multiple animations are multiplied together, so you get smooth transitions that combine properly.
Staggered Animations
Create sequenced animations by offsetting start times:
export default function StaggeredList({ tw, items }) {
return (
<div style={tw('flex flex-col gap-4')}>
{/* First item: starts at 0ms, lasts 300ms */}
<div style={tw('ease-out enter-fade-in-left/0/300')}>
{items[0]}
</div>
{/* Second item: starts at 100ms, lasts 300ms */}
<div style={tw('ease-out enter-fade-in-left/100/300')}>
{items[1]}
</div>
{/* Third item: starts at 200ms, lasts 300ms */}
<div style={tw('ease-out enter-fade-in-left/200/300')}>
{items[2]}
</div>
</div>
);
}
Dynamic Staggering
For dynamic lists, calculate the timing programmatically:
export default function DynamicStagger({ tw, items }) {
return (
<div style={tw('flex flex-col gap-4')}>
{items.map((item, i) => {
const start = i * 100; // Each item starts 100ms later
const duration = 300; // Each animation lasts 300ms
return (
<div
key={i}
style={tw(`ease-out enter-fade-in-up/${start}/${duration}`)}
>
{item}
</div>
);
})}
</div>
);
}
Common Patterns
Intro Sequence
export default function IntroVideo({ tw, title, subtitle, logo }) {
return (
<div style={tw('flex flex-col items-center justify-center w-full h-full bg-gradient-to-br from-blue-600 to-purple-700')}>
{/* Logo appears first */}
<img
src={logo}
style={tw('h-20 mb-8 ease-out enter-scale-in/0/300')}
/>
{/* Title bounces in */}
<h1 style={tw('text-7xl font-bold text-white ease-out enter-bounce-in-up/200/500')}>
{title}
</h1>
{/* Subtitle fades in last */}
<p style={tw('text-2xl text-white/80 mt-4 ease-out enter-fade-in-up/400/700')}>
{subtitle}
</p>
</div>
);
}
Text Reveal
export default function TextReveal({ tw, words }) {
return (
<div style={tw('flex flex-wrap gap-2 justify-center')}>
{words.split(' ').map((word, i) => (
<span
key={i}
style={tw(`text-4xl font-bold ease-out enter-fade-in-up/${i * 100}/200`)}
>
{word}
</span>
))}
</div>
);
}
Looping Background Element
export default function AnimatedBackground({ tw, children }) {
return (
<div style={tw('relative w-full h-full')}>
{/* Floating background circles */}
<div style={tw('absolute top-10 left-10 w-20 h-20 rounded-full bg-white/10 loop-float/2000')} />
<div style={tw('absolute bottom-20 right-20 w-32 h-32 rounded-full bg-white/10 loop-fade/1500')} />
{/* Main content */}
<div style={tw('relative z-10')}>
{children}
</div>
</div>
);
}
Full Enter/Exit Animation
export default function FullAnimation({ tw, title }) {
return (
<div style={tw('flex items-center justify-center w-full h-full bg-black')}>
{/* Enter: starts at 0, lasts 400ms. Exit: starts at 2600ms, lasts 400ms */}
<h1 style={tw('text-8xl font-bold text-white ease-out enter-bounce-in-up/0/400 exit-fade-out-up/2600/400')}>
{title}
</h1>
</div>
);
}
Programmatic Animations
For complete control beyond animation classes, use progress and frame directly.
Available Props
| Prop | Type | Description |
|---|---|---|
progress | number | 0 to 1 through the video (0% to 100%) |
frame | number | Current frame number (0, 1, 2, … totalFrames-1) |
These are only available in video templates. Use them when animation classes aren’t flexible enough.
Using frame
export default function FrameAnimation({ tw, frame, title }) {
// Color cycling using frame number
const hue = (frame * 5) % 360; // Cycle through colors
// Pulsing based on frame
const fps = 30;
const pulse = Math.sin(frame / fps * Math.PI * 2) * 0.2 + 0.8; // 0.6 to 1.0
return (
<div style={tw('flex items-center justify-center w-full h-full bg-black')}>
<h1 style={{
...tw('text-8xl font-bold'),
color: `hsl(${hue}, 70%, 60%)`,
transform: `scale(${pulse})`
}}>
{title}
</h1>
</div>
);
}
Using progress
export default function ProgressAnimation({ tw, progress, title }) {
// Custom fade based on progress
const opacity = progress < 0.3 ? progress / 0.3 : 1;
// Custom scale based on progress
const scale = 0.8 + progress * 0.2; // 0.8 to 1.0
return (
<div style={tw('flex items-center justify-center w-full h-full bg-gray-900')}>
<h1 style={{
...tw('text-8xl font-bold text-white'),
opacity,
transform: `scale(${scale})`
}}>
{title}
</h1>
</div>
);
}
Custom Easing
export default function CustomEasing({ tw, progress, title }) {
// Smoothstep easing
const eased = progress * progress * (3 - 2 * progress);
// Elastic easing
const elastic = Math.pow(2, -10 * progress) * Math.sin((progress - 0.075) * (2 * Math.PI) / 0.3) + 1;
return (
<div style={tw('flex items-center justify-center w-full h-full')}>
<h1 style={{
...tw('text-8xl font-bold'),
opacity: eased,
transform: `translateY(${(1 - elastic) * 100}px)`
}}>
{title}
</h1>
</div>
);
}
When to Use Programmatic Animations
Use progress/frame instead of animation classes when you need:
- Custom easing functions (elastic, bounce with specific curves beyond built-in ease-spring)
- Color cycling or gradients based on time
- Mathematical animations (sine waves, spirals, etc.)
- Complex multi-property animations that need precise coordination
- Conditional logic based on specific frame numbers
For everything else, prefer animation classes - they’re simpler and more maintainable.
Animating Along Paths
Animate elements along SVG paths with proper rotation using built-in path helpers:
export default function PathFollowing({ tw, progress, path }) {
// Follow a quadratic Bezier curve - one line!
const rocket = path.followQuadratic(
{ x: 200, y: 400 }, // Start point
{ x: 960, y: 150 }, // Control point
{ x: 1720, y: 400 }, // End point
progress
);
return (
<div style={{ display: 'flex', ...tw('relative w-full h-full bg-gray-900') }}>
{/* Draw the path (optional) */}
<svg width="1920" height="1080" style={{ position: 'absolute' }}>
<path
d="M 200 400 Q 960 150 1720 400"
stroke="rgba(255,255,255,0.2)"
strokeWidth={2}
fill="none"
/>
</svg>
{/* Element following the path */}
<div
style={{
position: "absolute",
left: rocket.x,
top: rocket.y,
transform: `translate(-50%, -50%) rotate(${rocket.angle}deg)`,
fontSize: '48px'
}}
>
🚀
</div>
</div>
);
}
Text Path Animations
Combine textPath helpers with animation classes to create animated text along curves:
Rotating text around a circle:
export default function RotatingCircleText({ tw, textPath, progress }) {
return (
<div style={tw('relative w-full h-full bg-black')}>
{/* Text rotates around circle using progress */}
{textPath.onCircle(
"SPINNING TEXT • AROUND • ",
960, // center x
540, // center y
400, // radius
progress, // rotation offset (0-1 animates full rotation)
{
fontSize: "3xl",
fontWeight: "bold",
color: "yellow-300"
}
)}
</div>
);
}
Animated text reveal along a path:
export default function PathTextReveal({ tw, textPath, progress }) {
// Create custom path follower that animates position
const pathFollower = (t) => {
// Only show characters up to current progress
const visibleProgress = progress * 1.5; // Extend range for smooth reveal
const opacity = t < visibleProgress ? 1 : 0;
// Follow quadratic curve
const pos = {
x: (1 - t) * (1 - t) * 200 + 2 * (1 - t) * t * 960 + t * t * 1720,
y: (1 - t) * (1 - t) * 400 + 2 * (1 - t) * t * 150 + t * t * 400,
angle: 0
};
return { ...pos, opacity };
};
return (
<div style={tw('relative w-full h-full bg-gray-900')}>
{textPath.onPath(
"REVEALING TEXT",
pathFollower,
{
fontSize: "4xl",
fontWeight: "bold",
color: "blue-300"
}
).map((char, i) => (
<div key={i} style={{ ...char.props.style, opacity: char.props.style.opacity || 1 }}>
{char}
</div>
))}
</div>
);
}
Staggered character entrance:
export default function StaggeredCircleText({ tw, textPath }) {
const text = "HELLO WORLD";
return (
<div style={tw('relative w-full h-full bg-slate-900')}>
{textPath.onCircle(
text,
960, 540, 400, 0,
{ fontSize: "4xl", fontWeight: "bold", color: "white" }
).map((char, i) => {
// Stagger fade-in: each character starts 50ms later
const staggerDelay = i * 50;
return (
<div
key={i}
style={{
...char.props.style,
...tw(`enter-fade-in/${staggerDelay}/300 enter-scale-100/${staggerDelay}/300`)
}}
>
{char.props.children}
</div>
);
})}
</div>
);
}
Text with bounce entrance along arc:
export default function BouncyArcText({ tw, textPath }) {
return (
<div style={tw('relative w-full h-full bg-gradient-to-br from-purple-600 to-blue-500')}>
{/* Draw the arc path */}
<svg width="1920" height="1080" style={{ position: 'absolute' }}>
<path
d="M 300 900 A 600 600 0 0 1 1620 900"
stroke="rgba(255,255,255,0.2)"
strokeWidth={2}
fill="none"
strokeDasharray="5 5"
/>
</svg>
{/* Text follows arc with staggered bounce */}
{textPath.onArc(
"BOUNCING ON ARC",
960, // cx
300, // cy
600, // radius
180, // start angle
360, // end angle
{ fontSize: "3xl", fontWeight: "bold", color: "white" }
).map((char, i) => (
<div
key={i}
style={{
...char.props.style,
...tw(`ease-out enter-bounce-in-up/${i * 80}/500`)
}}
>
{char.props.children}
</div>
))}
</div>
);
}
Loop animation with text on curve:
export default function LoopingCurveText({ tw, textPath, frame }) {
// Calculate wave effect using frame
const waveOffset = Math.sin(frame / 30 * Math.PI * 2) * 0.1;
return (
<div style={tw('relative w-full h-full bg-black')}>
{textPath.onQuadratic(
"WAVY TEXT",
{ x: 200, y: 400 },
{ x: 960, y: 150 },
{ x: 1720, y: 400 },
{ fontSize: "4xl", fontWeight: "bold", color: "pink-300" }
).map((char, i) => (
<div
key={i}
style={{
...char.props.style,
transform: `${char.props.style.transform} translateY(${Math.sin((i + frame) / 5) * 10}px)`
}}
>
{char.props.children}
</div>
))}
</div>
);
}
Tips for animating text paths:
- Use
progressfor smooth rotation on circles and arcs - Map over returned characters to apply individual animations
- Combine with animation classes like
enter-fade-in,enter-bounce-in, etc. - Stagger character animations by calculating delays:
i * delayMs - Use
framefor continuous effects like waves or pulsing - Preserve the original transform when adding animations:
transform: '${char.props.style.transform} ...'
Common path types:
Quadratic Bezier (Q command):
// Position: (1-t)²·P0 + 2(1-t)t·P1 + t²·P2
function pointOnQuadraticBezier(p0, p1, p2, t) {
const x = (1 - t) * (1 - t) * p0.x + 2 * (1 - t) * t * p1.x + t * t * p2.x;
const y = (1 - t) * (1 - t) * p0.y + 2 * (1 - t) * t * p1.y + t * t * p2.y;
return { x, y };
}
// Tangent angle
function angleOnQuadraticBezier(p0, p1, p2, t) {
const dx = 2 * (1 - t) * (p1.x - p0.x) + 2 * t * (p2.x - p1.x);
const dy = 2 * (1 - t) * (p1.y - p0.y) + 2 * t * (p2.y - p1.y);
return Math.atan2(dy, dx) * (180 / Math.PI);
}
Cubic Bezier (C command):
// Position: (1-t)³·P0 + 3(1-t)²t·P1 + 3(1-t)t²·P2 + t³·P3
function pointOnCubicBezier(p0, p1, p2, p3, t) {
const mt = 1 - t;
const mt2 = mt * mt;
const mt3 = mt2 * mt;
const t2 = t * t;
const t3 = t2 * t;
const x = mt3 * p0.x + 3 * mt2 * t * p1.x + 3 * mt * t2 * p2.x + t3 * p3.x;
const y = mt3 * p0.y + 3 * mt2 * t * p1.y + 3 * mt * t2 * p2.y + t3 * p3.y;
return { x, y };
}
// Tangent angle
function angleOnCubicBezier(p0, p1, p2, p3, t) {
const mt = 1 - t;
const mt2 = mt * mt;
const t2 = t * t;
const dx = -3 * mt2 * p0.x + 3 * mt2 * p1.x - 6 * mt * t * p1.x - 3 * t2 * p2.x + 6 * mt * t * p2.x + 3 * t2 * p3.x;
const dy = -3 * mt2 * p0.y + 3 * mt2 * p1.y - 6 * mt * t * p1.y - 3 * t2 * p2.y + 6 * mt * t * p2.y + 3 * t2 * p3.y;
return Math.atan2(dy, dx) * (180 / Math.PI);
}
Circle:
function pointOnCircle(cx, cy, radius, angleRadians) {
return {
x: cx + radius * Math.cos(angleRadians),
y: cy + radius * Math.sin(angleRadians)
};
}
// Usage
const angleRadians = progress * Math.PI * 2;
const pos = pointOnCircle(300, 300, 100, angleRadians);
const tangentAngle = (angleRadians * 180 / Math.PI) + 90; // Tangent is perpendicular
Tips:
- Use
progress(0-1) for smooth animation - The
translate(-50%, -50%)centers the element on the path - Combine rotation with the translate:
translate(-50%, -50%) rotate(${angle}deg) - For text following a path, you can animate individual characters at different progress values
SVG Stroke Animations
Animate SVG path strokes with the stroke-dash classes, perfect for drawing or erasing line art, icons, and illustrations.
How It Works
SVG stroke animations use strokeDasharray and strokeDashoffset CSS properties to create drawing effects:
- Enter animations - Draw the stroke from start to finish
- Exit animations - Erase the stroke from finish to start
- Loop animations - Continuously draw and erase
Format
All stroke-dash animations require the path length in brackets:
enter-stroke-dash-[length]/start/duration
exit-stroke-dash-[length]/start/duration
loop-stroke-dash-[length]/duration
Basic Examples
export default function SVGAnimation({ tw }) {
return (
<svg width="400" height="200" viewBox="0 0 400 200">
{/* Draw a curve over 1 second */}
<path
d="M10 150 Q 95 10 180 150"
stroke="black"
strokeWidth={4}
fill="none"
style={tw('enter-stroke-dash-[300]/0/1000')}
/>
</svg>
);
}
Enter Animations (Drawing)
Draw strokes from 0% to 100%:
// Draw a 300px path over 1 second
<path style={tw('enter-stroke-dash-[300]/0/1000')} />
// Draw with spring easing
<path style={tw('ease-spring enter-stroke-dash-[500]/0/1500')} />
// Stagger multiple paths
<path style={tw('enter-stroke-dash-[200]/0/600')} />
<path style={tw('enter-stroke-dash-[200]/200/600')} />
<path style={tw('enter-stroke-dash-[200]/400/600')} />
Exit Animations (Erasing)
Erase strokes from 100% to 0%:
// Erase starting at 2000ms, lasting 500ms
<path style={tw('exit-stroke-dash-[300]/2000/500')} />
// Draw then erase the same path
<path style={tw('enter-stroke-dash-[400]/0/800 exit-stroke-dash-[400]/2200/800')} />
Loop Animations
Continuously draw and erase:
// Loop every 2 seconds (draws in first half, erases in second half)
<path style={tw('loop-stroke-dash-[300]/2000')} />
// Faster loop
<path style={tw('loop-stroke-dash-[200]/1000')} />
Getting Path Length
To find the path length for your SVG:
// In browser console or component:
const path = document.querySelector('path');
const length = path.getTotalLength();
console.log(length); // e.g., 347.89
Then use that value:
<path style={tw('enter-stroke-dash-[347.89]/0/1000')} />
Complete Example
export default function DrawingEffect({ tw }) {
return (
<div style={tw('flex items-center justify-center w-full h-full bg-gray-900')}>
<svg width="600" height="400" viewBox="0 0 600 400">
{/* Checkmark icon drawn in sequence */}
<path
d="M100 200 L 200 300 L 400 100"
stroke="#10b981"
strokeWidth={8}
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
style={tw('ease-out enter-stroke-dash-[600]/0/1200')}
/>
{/* Circle drawn after checkmark */}
<circle
cx="250"
cy="200"
r="150"
stroke="#10b981"
strokeWidth={6}
fill="none"
style={tw('ease-out enter-stroke-dash-[942]/1000/1000')}
/>
</svg>
</div>
);
}
Combining with Other Animations
Stroke animations work alongside other animation classes:
// Fade in while drawing
<path style={tw('enter-stroke-dash-[300]/0/1000 enter-fade-in/0/1000')} />
// Draw with pulsing color
<svg>
<path
stroke="url(#gradient)"
style={tw('enter-stroke-dash-[500]/0/1500')}
/>
<defs>
<linearGradient id="gradient">
<stop offset="0%" stopColor="#8b5cf6" />
<stop offset="100%" stopColor="#ec4899" />
</linearGradient>
</defs>
</svg>
Animated Dashed Strokes (Marching Ants)
For marching ants or animated dashed patterns, use frame or progress directly instead of animation classes:
export default function MarchingAnts({ tw, frame }) {
// Calculate animated offset (loops every 30 frames)
const dashOffset = -(frame % 30) * 2;
return (
<div style={tw('flex items-center justify-center w-full h-full bg-gray-900')}>
<svg width="600" height="400" viewBox="0 0 600 400">
{/* Marching ants border */}
<rect
x="50"
y="50"
width="500"
height="300"
fill="none"
stroke="#3b82f6"
strokeWidth={3}
strokeDasharray="10 5"
strokeDashoffset={dashOffset}
/>
{/* Animated circle with different speed */}
<circle
cx="300"
cy="200"
r="80"
fill="none"
stroke="#10b981"
strokeWidth={4}
strokeDasharray="15 8"
strokeDashoffset={dashOffset * 1.5}
/>
</svg>
</div>
);
}
Tips:
strokeDasharray="10 5"- 10px dash, 5px gapstrokeDashoffset={dashOffset}- animates the pattern position- Negative offset moves forward, positive moves backward
- Different speeds: multiply by different values (e.g.,
dashOffset * 2)
This technique is different from stroke-dash classes:
stroke-dashclasses - Draw/erase the stroke (reveal animation)- Marching ants - Move a dashed pattern along the stroke
Performance Tips
- Use Tailwind classes when possible - they’re optimized for the renderer
- Avoid too many nested animations - each adds computation per frame
- Use loop animations sparingly - they’re computed every frame
- Prefer opacity and transform - they’re the most performant properties