Generate images and videos from React + Tailwind CSS templates using the loopwind CLI.
使用 loopwind CLI 从 React + Tailwind CSS 模板生成图片和视频
tomtev
内容创作clawhubv0.25.111 版本100000Key: 无需
★ 0
Stars
📥 1,166
下载
💾 38
安装
1
版本
#latest
概述
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.
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 = 20px enter-translate-x-full = 100% enter-translate-x-[20px] = 20px
// 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 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>
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.
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 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
// 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:
{
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
}
Examples
Animated rotating text:
export default function RotatingText({ tw, textPath, progress }) {
return (
<div style={tw('relative w-full h-full bg-black')}>
{textPath.onCircle(
"SPINNING • TEXT • ",
960, 540, 400,
progress, // Rotate based on video progress
{ fontSize: "3xl", color: "yellow-300" }
)}
</div>
);
}
Multiple text paths:
export default function MultiPath({ tw, textPath }) {
return (
<div style={tw('relative w-full h-full bg-gradient-to-br from-slate-900 to-slate-700')}>
{/* Text on outer circle */}
{textPath.onCircle(
"OUTER RING",
960, 540, 500, 0,
{ fontSize: "5xl", fontWeight: "bold", color: "white" }
)}
{/* Text on inner circle */}
{textPath.onCircle(
"inner ring",
960, 540, 300, 0.5, // offset by 50% for rotation
{ fontSize: "2xl", color: "white/60" }
)}
</div>
);
}
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
}
// Primary text
tw('text-foreground')
// Secondary/muted text
tw('text-muted-foreground')
// Accent/brand text
tw('text-primary')
// Destructive/error text
tw('text-destructive')
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
// 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
// Complete gradient
tw('bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500')
<h1 style={tw('font-sans font-bold')}>
{/* Uses Inter Bold from loopwind.json */}
{title}
</h1>
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:
loopwind.json fonts (if configured with files)
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
✅ Use loopwind.json for project-wide fonts - Configure once, use everywhere
✅ Use font classes - tw('font-sans') instead of fontFamily: 'Inter'
✅ Include fallbacks - Always add system fonts: ["Inter", "system-ui", "sans-serif"]
✅ Match names - First font in family array is used as the loaded font name
✅ Relative paths - Font paths are relative to loopwind.json location