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:

TypeFormatDescription
Enterenter-{type}/{start}/{duration}Animations that play when entering
Exitexit-{type}/{start}/{duration}Animations that play when exiting
Looploop-{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

UtilityFormatDescriptionExample
translate-xenter-translate-x-{value}Translate horizontallyenter-translate-x-5 = 20px
enter-translate-x-full = 100%
enter-translate-x-[20px] = 20px
translate-yenter-translate-y-{value}Translate verticallyloop-translate-y-10 = 40px
enter-translate-y-1/2 = 50%
enter-translate-y-[5rem] = 80px
opacityenter-opacity-{n}Set opacity (0-100)enter-opacity-50 = 50%
scaleenter-scale-{n}Scale element (0-200)enter-scale-100 = 1.0x
rotateenter-rotate-{n}Rotate in degreesenter-rotate-45 = 45°
skew-xenter-skew-x-{n}Skew on X axis in degreesenter-skew-x-12 = 12°
skew-yenter-skew-y-{n}Skew on Y axis in degreesexit-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>
ClassDescription
enter-fade-in/0/500Fade in (opacity 0 → 1)
enter-fade-in-up/0/500Fade in + slide up (30px)
enter-fade-in-down/0/500Fade in + slide down (30px)
enter-fade-in-left/0/500Fade in + slide from left (30px)
enter-fade-in-right/0/500Fade 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>
ClassDescription
enter-slide-left/0/500Slide in from left (100px)
enter-slide-right/0/500Slide in from right (100px)
enter-slide-up/0/500Slide in from bottom (100px)
enter-slide-down/0/500Slide 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>
ClassDescription
enter-bounce-in/0/500Bounce in with scale overshoot
enter-bounce-in-up/0/500Bounce in from below
enter-bounce-in-down/0/500Bounce in from above
enter-bounce-in-left/0/500Bounce in from left
enter-bounce-in-right/0/500Bounce 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>
ClassDescription
enter-scale-in/0/500Scale up from 50% to 100%
enter-zoom-in/0/500Zoom 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>
ClassDescription
enter-rotate-in/0/500Rotate in from -180°
enter-flip-in-x/0/5003D flip on horizontal axis
enter-flip-in-y/0/5003D flip on vertical axis

Exit Animations

Format: exit-{type}/{startMs}/{durationMs}

  • startMs - when the exit animation begins
  • durationMs - 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>
ClassDescription
exit-fade-out/2500/500Fade out (opacity 1 → 0)
exit-fade-out-up/2500/500Fade out + slide up
exit-fade-out-down/2500/500Fade out + slide down
exit-fade-out-left/2500/500Fade out + slide left
exit-fade-out-right/2500/500Fade out + slide right
exit-slide-up/2500/500Slide out upward (100px)
exit-slide-down/2500/500Slide out downward (100px)
exit-slide-left/2500/500Slide out to left (100px)
exit-slide-right/2500/500Slide out to right (100px)
exit-scale-out/2500/500Scale out to 150%
exit-zoom-out/2500/500Zoom out to 200%
exit-rotate-out/2500/500Rotate out to 180°
exit-bounce-out/2500/500Bounce out with scale
exit-bounce-out-up/2500/500Bounce out upward
exit-bounce-out-down/2500/500Bounce out downward
exit-bounce-out-left/2500/500Bounce out to left
exit-bounce-out-right/2500/500Bounce 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>
ClassDescription
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>
ClassDescriptionBest For
linearConstant speedMechanical motion
ease-inSlow start, fast endExit animations
ease-outFast start, slow end (default)Enter animations
ease-in-outSlow start and endSubtle transitions
ease-in-cubicStrong slow startDramatic exits
ease-out-cubicStrong fast startImpactful entrances
ease-in-out-cubicStrong both endsEmphasis animations
ease-in-quartVery strong slow startPowerful exits
ease-out-quartVery strong fast startPunchy entrances
ease-in-out-quartVery strong both endsMaximum 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:

  1. Default easing (ease-*) applies to ALL animations if no specific override is set
  2. Specific easing (enter-ease-*, exit-ease-*, loop-ease-*) overrides the default for that animation type
  3. If both are present, specific easing takes priority for its animation type

Available easing classes:

Default (all animations)Enter onlyExit onlyLoop only
ease-inenter-ease-inexit-ease-inloop-ease-in
ease-outenter-ease-outexit-ease-outloop-ease-out
ease-in-outenter-ease-in-outexit-ease-in-outloop-ease-in-out
ease-in-cubicenter-ease-in-cubicexit-ease-in-cubicloop-ease-in-cubic
ease-out-cubicenter-ease-out-cubicexit-ease-out-cubicloop-ease-out-cubic
ease-in-out-cubicenter-ease-in-out-cubicexit-ease-in-out-cubicloop-ease-in-out-cubic
ease-in-quartenter-ease-in-quartexit-ease-in-quartloop-ease-in-quart
ease-out-quartenter-ease-out-quartexit-ease-out-quartloop-ease-out-quart
ease-in-out-quartenter-ease-in-out-quartexit-ease-in-out-quartloop-ease-in-out-quart
linearenter-ease-linearexit-ease-linearloop-ease-linear
ease-springenter-ease-springexit-ease-springloop-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:

ParameterDescriptionEffect when increasedDefault
massMass of the springSlower, more inertia1
stiffnessSpring stiffnessFaster, snappier100
dampingDamping coefficientLess bounce, smoother10

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:

  1. Default ease-spring - Uses a pre-calculated spring curve optimized for most use cases
  2. Custom ease-spring/mass/stiffness/damping - Generates a physics-based spring curve using the damped harmonic oscillator formula
  3. The spring automatically calculates its ideal duration to reach the final state
  4. 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

PropTypeDescription
progressnumber0 to 1 through the video (0% to 100%)
framenumberCurrent 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:

  1. Use progress for smooth rotation on circles and arcs
  2. Map over returned characters to apply individual animations
  3. Combine with animation classes like enter-fade-in, enter-bounce-in, etc.
  4. Stagger character animations by calculating delays: i * delayMs
  5. Use frame for continuous effects like waves or pulsing
  6. 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:

  1. Enter animations - Draw the stroke from start to finish
  2. Exit animations - Erase the stroke from finish to start
  3. 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 gap
  • strokeDashoffset={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-dash classes - Draw/erase the stroke (reveal animation)
  • Marching ants - Move a dashed pattern along the stroke

Performance Tips

  1. Use Tailwind classes when possible - they’re optimized for the renderer
  2. Avoid too many nested animations - each adds computation per frame
  3. Use loop animations sparingly - they’re computed every frame
  4. Prefer opacity and transform - they’re the most performant properties

Next Steps

  • Templates - Creating image and video templates
  • Helpers - QR codes, images, and more