name: loopwind
description: Generate images and videos from React + Tailwind CSS templates using the loopwind CLI.
metadata:
version: "0.25.11"
loopwind
A CLI tool for generating images and videos from JSX templates using Tailwind CSS and Satori. Templates live in a .loopwind/ directory alongside your codebase.
Quick Start
Loopwind is a CLI tool for generating images and videos with React and Tailwind CSS. It's designed to be used with AI Agents and Cursor.
Installation
curl -fsSL https://loopwind.dev/install.sh | bash
This installs loopwind to ~/.loopwind/ and adds the loopwind command to your PATH. Requires Node.js 18+.
Initialize in Your Project
Navigate to any project folder and run:
loopwind init
This creates .loopwind/loopwind.json β a configuration file with your project's theme colors.
Install AI Skill
Give your AI agent expertise in loopwind:
npx skills add https://loopwind.dev/skill.md
This installs a skill that teaches Claude Code (or other AI agents) how to create templates, use animation classes, and render images/videos.
Use with Claude Code
With the loopwind skill installed, Claude has deep knowledge of template structure, animation classes, and Tailwind CSS patterns for Satori. Just ask:
Create an OG image for my blog post about TypeScript tips
Create an animated intro video for my YouTube channel
Claude will create optimized templates and render the final output automatically.
Templates are React components that define your images and videos. They use Tailwind CSS for styling and export metadata that loopwind uses for rendering.
Layouts let you wrap templates with consistent headers, footers, and styling. A child template specifies a layout in its meta, and the layout receives the child content as a children prop.
{/* Bounce in from below: starts at 0, lasts 400ms */}
{title}
{/* Fade in with upward motion: starts at 300ms, lasts 400ms */}
{subtitle}
{/* Continuous floating animation: repeats every 1s (1000ms) */}
β¬οΈ
);
}
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
Content
// Rotate 90 degrees on entrance
Spinning
// Fade to 50% opacity in a loop
Pulsing
// Scale down with negative value
Shrinking
Supported Utilities
| Utility | Format | Description | Example |
|---------|--------|-------------|---------|
| translate-x | enter-translate-x-{value} | Translate horizontally | enter-translate-x-5 = 20px enter-translate-x-full = 100% enter-translate-x-[20px] = 20px |
| translate-y | enter-translate-y-{value} | Translate vertically | loop-translate-y-10 = 40px enter-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)
// Continuous rotation in loop (360 degrees per cycle)
Spinning
// Rotate backwards with negative value
Counter Rotation
Skew Animations
// Skew on X axis
Slanted
// Skew on Y axis
Tilted
// Oscillating skew in loop
Wobbling
// Negative skew
Reverse Slant
Combining Utilities
You can combine multiple utility animations on the same element:
// Translate and rotate together
Flying In
// Fade and scale
Appearing
// Enter with translate, exit with rotation
Slide and Spin
Bracket Notation
For more CSS-like syntax, you can use brackets with units:
// Using bracket notation with seconds
Hello
// Using bracket notation with milliseconds
World
// Mix and match - plain numbers are milliseconds
Mixed
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
Hello
// Fade in with upward motion
Hello
| 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
Content
// Slide up from bottom: starts at 200ms, lasts 600ms
Content
| 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
Bouncy!
// Bounce in from below
Pop!
| 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%
Growing
// Zoom in from 0%
Zooming
| 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
Spinning
// 3D flip on X axis
Flipping
| 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 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)
Goodbye
// Combined enter and exit on same element
Hello and Goodbye
| 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
Pulsing
// Bounce every 800ms
Bouncing
// Full rotation every 2000ms
Spinning
| 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)
Accelerating
// Ease out (decelerate) - default
Decelerating
// Ease in-out (smooth)
Smooth
// Strong cubic easing
Dramatic
| 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
Smooth entrance, sharp exit
// Loop with linear easing, enter with bounce
Bouncy entrance, linear loop
// Default easing still works (applies to all animations)
Same easing for both
// Mix default with specific overrides
Default ease-out for enter, cubic-in for exit
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
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
Bouncy spring!
// Per-animation-type spring
Spring entrance, smooth exit
// Custom spring with parameters: ease-spring/mass/stiffness/damping
Custom spring (mass=1, stiffness=100, damping=10)
// More bouncy spring (lower damping)
Extra bouncy!
// Stiffer spring (higher stiffness, faster)
Snappy spring
// Per-animation-type custom springs
Different springs for enter and exit
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:
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 (
{/* Fade in during first 500ms, fade out during last 500ms (assuming 3s video) */}
{title}
);
}
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 (
{/* First item: starts at 0ms, lasts 300ms */}
{items[0]}
{/* Second item: starts at 100ms, lasts 300ms */}
{items[1]}
{/* Third item: starts at 200ms, lasts 300ms */}
{items[2]}
);
}
Dynamic Staggering
For dynamic lists, calculate the timing programmatically:
export default function DynamicStagger({ tw, items }) {
return (
{items.map((item, i) => {
const start = i * 100; // Each item starts 100ms later
const duration = 300; // Each animation lasts 300ms
export default function AnimatedBackground({ tw, children }) {
return (
{/* Floating background circles */}
{/* Main content */}
{children}
);
}
Full Enter/Exit Animation
export default function FullAnimation({ tw, title }) {
return (
{/* Enter: starts at 0, lasts 400ms. Exit: starts at 2600ms, lasts 400ms */}
{title}
);
}
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
{/* 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"
}
)}
);
}
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;
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 };
}
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:
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
How it works:
1. template(name, props) renders another installed template
2. The embedded template is rendered at its specified size
3. You can embed multiple templates in one design
4. Templates can be nested (template within a template)
// Render templates based on data
{users.map(user =>
template('user-avatar', { name: user.name, image: user.avatar })
)}
Best Practices
1. Keep templates focused - Each template should do one thing well
2. Pass minimal props - Only pass what the embedded template needs
3. Document dependencies - Note which templates are required in your README
4. Avoid deep nesting - Too many nested templates can be hard to debug
tsx
// Text along a circular arc
textPath.onArc(
"ARC TEXT",
960, // center x
540, // center y
400, // radius
0, // start angle (degrees)
180, // end angle (degrees)
{ fontSize: "2xl", color: "pink-300" }
)
Options
All
textPath functions accept an optional options object:
typescript
{
fontSize?: string; // Tailwind size: "xl", "2xl", "4xl", etc.
fontWeight?: string; // Tailwind weight: "bold", "semibold", etc.
color?: string; // Tailwind color: "white", "blue-500", etc.
letterSpacing?: number; // Space between characters (0-1, default: 0)
style?: any; // Additional inline styles
}
tsx
export default function MyTemplate({
// Core helpers (RESERVED - cannot be used as prop names)
tw, // Tailwind class converter
qr, // QR code generator (this page)
template, // Template composer (this page)
config, // User config from loopwind.json (this page)
textPath, // Text on path helpers (this page)
// Media helpers (RESERVED)
image, // Image embedder β see /images
path, // Path following β see /animation
// Video-specific (RESERVED - only in video templates)
frame, // Current frame number β see /templates
progress, // Animation progress 0-1 β see /templates
// Your custom props (use any names EXCEPT the reserved ones above)
...props // Any props from your meta.props
}) {
// Your template code
}
Use Tailwind's slash syntax for opacity with any color:
tsx
export default function OpacityExample({ tw }) {
return (
{/* 50% opacity */}
{/* 75% opacity */}
Subtle text
{/* 30% opacity */}
Faint border
);
}
Supported syntax:bg-{color}/{opacity} - Background with opacity
text-{color}/{opacity} - Text with opacity
border-{color}/{opacity} - Border with opacity
Text Hierarchy
tsx
// Primary text
tw('text-foreground')
// Secondary/muted text
tw('text-muted-foreground')
tsx
tw('text-blue-500') // Standard Tailwind color
tw('bg-purple-600') // Standard Tailwind color
tw('text-primary') // shadcn semantic color
tw('bg-card') // shadcn semantic color
tsx
// Gradient direction
tw('bg-gradient-to-r') // left to right
tw('bg-gradient-to-br') // top-left to bottom-right
tw('bg-gradient-to-t') // bottom to top
// Gradient colors
tw('from-blue-500') // Start color
tw('via-purple-500') // Middle color
tw('to-pink-500') // End color
tsx
tw('text-brand') // Uses your custom brand color
tw('bg-primary') // Uses your custom primary color
tw('bg-accent') // Uses your custom accent color
Auto-Detection from tailwind.config.js
loopwind automatically detects and loads your project's Tailwind configuration:
{/* Uses Inter Bold from loopwind.json */}
{title}
Available classes:font-sans - Uses fonts.sans from loopwind.json
font-serif - Uses fonts.serif from loopwind.json
font-mono - Uses fonts.mono from loopwind.json
Supported formats:
β WOFF (
.woff) - Recommended for best compatibility
β TTF (
.ttf) - Also supported
β OTF (
.otf) - Also supported
β WOFF2 (
.woff2) - Not supported by renderer
Font Loading Priority
loopwind loads fonts in this order:
1. loopwind.json fonts (if configured with
files)
2. Bundled Inter fonts (included with CLI)
This ensures fonts work out of the box with no configuration.
Default Fonts
If no fonts are configured, loopwind uses Inter (Regular 400, Bold 700) which is bundled with the CLI. This means fonts work offline with no configuration required.
Best Practices
1. β Use loopwind.json for project-wide fonts - Configure once, use everywhere
2. β Use font classes -
tw('font-sans') instead of fontFamily: 'Inter'
3. β Include fallbacks - Always add system fonts: ["Inter", "system-ui", "sans-serif"]
4. β Match names - First font in family array is used as the loaded font name
5. β Relative paths - Font paths are relative to loopwind.json location
1. Keep templates focused - Each template should do one thing well
2. Pass minimal props - Only pass what the embedded template needs
3. Document dependencies - Note which templates are required in your README
4. Avoid deep nesting - Too many nested templates can be hard to debug