| 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.
Install a Template
1. Official Templates
loopwind add image-template
loopwind add video-template
Templates are installed to: .loopwind/<template>/
Benefits:
- Templates are local to your project
- Version controlled with your project
- Easy to share within your team
Render a Template
loopwind render template-name '{"title":"Hello World","subtitle":"Built with loopwind"}'
or use a local props file:
loopwind render template-name props.json
Commands
loopwind add <source>
Install a template from various sources:
loopwind add image-template
loopwind add video-template
These will be downloaded to .loopwind/<template>/
loopwind list
List all installed templates:
loopwind list
loopwind render <template> <props> [options]
Render an image or video:
loopwind render banner-hero '{"title":"Hello World"}'
loopwind render video-intro '{"title":"Welcome"}'
loopwind render banner-hero props.json
loopwind render banner-hero '{"title":"Hello"}' --out custom-name.png
loopwind render banner-hero '{"title":"Hello"}' --format jpeg
Options:
--out, -o - Output filename (default: <template>.<ext> in current directory)
--format - Output format: png, jpeg, svg (images only)
--quality - JPEG quality 1-100 (default: 92)
loopwind validate <template>
Validate a template:
loopwind validate banner-hero
Checks:
- Template file exists and is valid React
export const meta exists and is valid
- Required props are defined
- Fonts exist (if specified)
loopwind init
Initialize loopwind in a project:
loopwind init
Creates .loopwind/loopwind.json configuration file with your project's design tokens.
Animation Classes (Video Only)
Use Tailwind-style animation classes - no manual calculations needed:
<h1 style={tw('enter-fade-in/0/500')}>Hello</h1>
<div style={tw('loop-ping/500')} />
<h1 style={tw('ease-out enter-bounce-in-up/0/600')}>Title</h1>
See Animation for the complete reference.
Next Steps
Templates
Templates are React components that define your images and videos. They use Tailwind CSS for styling and export metadata that loopwind uses for rendering.
Installing Templates
Official Templates
loopwind add image-template
loopwind add video-template
Templates are installed to .loopwind/<template-name>/.
Direct URLs
loopwind add https://example.com/templates/my-template.json
Local Filesystem
loopwind add ./my-templates/banner-hero
loopwind add /Users/you/templates/social-card
Image Templates
Basic Structure
export const meta = {
name: "banner-hero",
type: "image",
description: "Hero banner with gradient background",
size: { width: 1600, height: 900 },
props: { title: "string", subtitle: "string" }
};
export default function BannerHero({ title, subtitle, tw }) {
return (
<div style={tw('flex flex-col justify-center items-center w-full h-full bg-gradient-to-br from-purple-600 to-blue-500 p-12')}>
<h1 style={tw('text-7xl font-bold text-white mb-4')}>
{title}
</h1>
<p style={tw('text-2xl text-white/80')}>
{subtitle}
);
}
Rendering Images
loopwind render banner-hero '{"title":"Hello World","subtitle":"Welcome"}'
loopwind render banner-hero '{"title":"Hello"}' --out custom-name.png
loopwind render banner-hero '{"title":"Hello"}' --format jpeg --quality 95
loopwind render banner-hero props.json
Output Formats
| Format | Best For |
|---|
| PNG (default) | Transparency, sharp text, logos |
| JPEG | Photographs, gradients, smaller files |
| SVG | Vector graphics, scalable designs |
Video Templates
Basic Structure
export const meta = {
name: "video-intro",
type: "video",
description: "Animated intro with bounce-in title",
size: { width: 1920, height: 1080 },
video: { fps: 30, duration: 3 },
props: { title: "string" }
};
export default function VideoIntro({ tw, title }) {
return (
<div style={tw('flex items-center justify-center w-full h-full bg-gradient-to-br from-blue-600 to-purple-700')}>
<h1 style={tw('text-8xl font-bold text-white ease-out enter-bounce-in-up/0/600')}>
{title}
</h1>
</div>
);
}
Rendering Videos
loopwind render video-intro '{"title":"Welcome!"}' --out intro.mp4
loopwind render video-intro '{"title":"Welcome!"}' --ffmpeg
loopwind render video-intro '{"title":"Welcome!"}' --crf 18
FPS and Duration
video: { fps: 30, duration: 3 }
| FPS | Use Case |
|---|
| 24 | Cinematic look, smaller files |
| 30 | Standard web video |
| 60 | Smooth animations |
Video-Specific Props
Templates receive these additional props:
frame - Current frame number (0 to totalFrames - 1)
progress - Animation progress from 0 to 1
export default function MyVideo({ frame, progress }) {
}
Encoding Options
| Encoder | Command | Use Case |
|---|
| WASM (default) | loopwind render ... | CI/CD, no dependencies |
| FFmpeg | loopwind render ... --ffmpeg | Faster, smaller files |
Install FFmpeg: brew install ffmpeg (macOS)
Animation Classes
Use Tailwind-style animation classes for videos:
<h1 style={tw('enter-fade-in/0/500')}>Fade in at start</h1>
<h1 style={tw('enter-bounce-in-up/300/400')}>Bounce in after 300ms</h1>
<div style={tw('exit-fade-out/2500/500')}>Fade out at 2.5s</div>
<div style={tw('loop-float/1000')}>Continuous floating</div>
<div style={tw('loop-spin/1000')}>Spinning</div>
<h1 style={tw('ease-out enter-slide-left/0/500')}>Smooth slide</h1>
See the full Animation documentation for all classes.
Common Sizes
Social Media
- Twitter/X Card: 1200x675
- Facebook/OG: 1200x630
- Instagram Post: 1080x1080
- LinkedIn Post: 1200x627
Web Graphics
- Hero Banner: 1920x1080
- Blog Header: 1600x900
- Thumbnail: 640x360
Example Templates
Open Graph Image
export const meta = {
name: "og-image",
type: "image",
size: { width: 1200, height: 630 },
props: { title: "string", description: "string" }
};
export default function OGImage({ tw, image, title, description }) {
return (
<div style={tw('flex w-full h-full bg-white')}>
<div style={tw('flex-1 flex flex-col justify-between p-12')}>
<img src={image('logo.svg')} style={tw('h-12 w-auto')} />
<div>
<h1 style={tw('text-5xl font-bold text-gray-900 mb-4')}>{title}</>
{description}
yoursite.com
);
}
Animated Intro
export const meta = {
name: "animated-intro",
type: "video",
size: { width: 1920, height: 1080 },
video: { fps: 60, duration: 3 },
props: { title: "string", subtitle: "string" }
};
export default function AnimatedIntro({ tw, title, subtitle }) {
return (
<div style={tw('flex flex-col items-center justify-center w-full h-full bg-background')}>
<h1 style={tw('text-8xl font-bold text-foreground ease-out enter-bounce-in-up/0/400')}>
{title}
</h1>
<p style={tw('text-2xl text-muted-foreground mt-4 //')}>
{subtitle}
);
}
Next Steps
Layouts
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.
Basic Usage
Layout Template
Create a layout template that receives children:
export const meta = {
name: 'base-layout',
type: 'image',
size: { width: 1200, height: 630 },
props: {},
};
export default function BaseLayout({ tw, children }) {
return (
<div style={tw('flex flex-col w-full h-full bg-background')}>
{/* Header */}
<div style={tw('flex items-center px-8 py-4 border-b border-border')}>
<span style={tw('text-2xl font-bold text-primary')}>loopwind</span>
</div>
{/* Content slot */}
<div style={tw('flex flex-1')}>
{children}
</div>
{/* Footer */}
loopwind.dev
);
}
Usage in Templates
Reference the layout using a relative path:
export const meta = {
name: 'blog-post',
type: 'image',
layout: '../base-layout',
props: {
title: 'string',
excerpt: 'string',
},
};
export default function BlogPost({ tw, title, excerpt }) {
return (
<div style={tw('flex flex-col justify-center p-12')}>
<h1 style={tw('text-5xl font-bold text-foreground mb-4 text-balance')}>
{title}
</h1>
<p style={tw('text-xl text-muted-foreground leading-relaxed')}>
{excerpt}
</p>
</div>
);
}
Render
loopwind render blog-post '{"title":"Hello World","excerpt":"My first post"}'
The output uses the layout's size (1200x630) with the child content inside.
Key Concepts
Size
When using a layout, the layout's size controls the final output dimensions. The child template doesn't need a size property.
Path Resolution
Use relative paths to reference layouts:
layout: '../base-layout'
layout: './shared/layout'
layout: '../../layouts/main'
Props Flow
The layout receives:
- All standard helpers (
tw, image, qr, template, etc.)
children prop containing the rendered child content
- Animation context (
frame, progress) for video layouts
export default function Layout({ tw, children, frame, progress }) {
return (
<div style={tw('flex w-full h-full')}>
{children}
</div>
);
}
Video Layouts
Layouts work with video templates. Both the layout and child can use animations:
export const meta = {
name: 'video-layout',
type: 'video',
size: { width: 1920, height: 1080 },
video: { fps: 60, duration: 4 },
props: {},
};
export default function VideoLayout({ tw, children }) {
return (
<div style={tw('flex flex-col w-full h-full bg-background')}>
{/* Animated header */}
<div style={tw('flex items-center px-12 py-6 ease-out enter-slide-down/0/500')}>
<span style={tw('text-3xl font-bold text-primary')}>loopwind</span>
</div>
{/* Content */}
<div =' ')}>
{children}
{/* Animated footer */}
loopwind.dev
);
}
Example: Consistent OG Images
Create a layout for all your OG images:
export const meta = {
name: 'og-layout',
type: 'image',
size: { width: 1200, height: 630 },
props: {},
};
export default function OGLayout({ tw, image, children }) {
return (
<div style={tw('flex w-full h-full bg-background')}>
{/* Content area */}
<div style={tw('flex flex-col flex-1 p-12')}>
{/* Logo */}
<div style={tw('flex items-center gap-3 mb-auto')}>
<img src={image('logo.svg')} style={tw('h-10 w-auto')} />
<span style={tw('text-2xl font-bold')}>MyBrand
{/* Slot for page-specific content */}
{children}
{/* Domain */}
mybrand.com
);
}
Then create page-specific templates:
export const meta = {
name: 'og-blog',
type: 'image',
layout: '../og-layout',
props: {
title: 'string',
author: 'string',
},
};
export default function OGBlog({ tw, title, author }) {
return (
<div style={tw('flex flex-col')}>
<span style={tw('text-sm text-muted-foreground uppercase tracking-wider mb-2')}>
Blog Post
</span>
<h1 style={tw('text-4xl font-bold text-foreground mb-4 text-balance')}>
{title}
</h1>
<span style={tw('text-muted-foreground')}>By {author}</span>
</div>
);
}
Next Steps
- Templates - Template structure and metadata
- Animation - Animation classes for video layouts
- Helpers - Using image(), qr(), and template()
Embedding Images
Use the image() helper to embed images in your templates. It supports loading from props, template directories, and URLs.
Prop-based Images
Pass the prop name to load an image path from props:
export const meta = {
name: "product-card",
type: "image",
size: { width: 1200, height: 630 },
props: {
title: "string",
background: "string?"
}
};
export default function ProductCard({ tw, image, title, background }) {
const bgSrc = background
? image('background')
: 'https://images.unsplash.com/photo-1557682250-33bd709cbe85?w=1200';
return (
<div style={tw('relative w-full h-full')}>
<img
src={bgSrc}
style={tw('absolute inset-0 w-full h-full object-cover')}
/>
<div style={tw('relative z-10 p-12')}>
<h1 style=' ')}>{title}
);
}
The image('background') helper loads from the background prop value (file path or URL).
Direct File Images
Load images directly from your template directory by including the file extension:
export default function ChangelogItem({ tw, image, text }) {
return (
<div style={tw('flex items-center gap-4')}>
{/* Load check.svg from template directory */}
<img
src={image('check.svg')}
style={tw('w-6 h-6')}
/>
<span style={tw('text-lg')}>{text}</span>
</div>
);
}
You can also use subdirectories:
<img src={image('assets/icons/star.svg')} />
<img src={image('shared/logo.png')} />
Template directory structure:
.loopwind/my-template/
├── template.tsx
├── check.svg ← image('check.svg')
└── assets/
└── icons/
└── star.svg ← image('assets/icons/star.svg')
URLs
The image() helper also supports loading images from URLs:
{
"background": "https://example.com/image.jpg"
}
Supported Formats
- JPEG (
.jpg, .jpeg)
- PNG (
.png)
- GIF (
.gif)
- WebP (
.webp)
- SVG (
.svg)
Image Positioning
Use Tailwind's object-fit utilities:
export default function ImageGrid({ tw, image, img1, img2, img3 }) {
return (
<div style={tw('flex gap-4 w-full h-full p-8 bg-gray-100')}>
{/* Cover - fills entire area, may crop */}
<img
src={image('img1')}
style={tw('w-full h-full object-cover rounded-lg')}
/>
{/* Contain - fits within area, may letterbox */}
<img
src={image('img2')}
style={tw('w-full h-full object-contain')}
/>
{/* Fill - stretches to fill */}
<img
src={image('img3')}
style={tw('w-full h-full object-fill')}
/>
</div>
);
}
Troubleshooting
Images Not Loading
Check file paths are relative to the props file:
{
"background": "./images/bg.jpg"
}
Absolute paths won't work.
Optimize Image Sizes
Use appropriately sized images before embedding:
convert large-image.jpg -resize 1600x900 optimized.jpg
Next Steps
- Templates - Creating image and video templates
- Animation - Animation classes for videos
- Styling - Tailwind & shadcn/ui integration
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:
<div style={tw('enter-translate-x-5/0/1000')}>Content</div>
<div style={tw('enter-rotate-90/0/500')}>Spinning</div>
<div style={tw('loop-opacity-50/1000')}>Pulsing</div>
<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 |
| 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)
- 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
<div style={tw('enter-translate-x-5/0/500')}>Content</div>
<div style={tw('enter-translate-y-full/0/800')}>Dropping full height</div>
<div style={tw('enter-translate-x-1/2/0/600')}>Slide in halfway</div>
<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>
<div style={tw('loop-translate-y-1/4/1000')}>Oscillate 25%</>
Opacity Animations
<div style={tw('enter-opacity-100/0/500')}>Fading In</div>
<div style={tw('enter-opacity-50/0/800')}>Half Opacity</div>
<div style={tw('loop-opacity-50/1000')}>Pulsing</div>
<div style={tw('exit-opacity-0/2500/500')}>Vanishing</div>
Scale Animations
<div style={tw('enter-scale-100/0/500')}>Growing</div>
<div style={tw('enter-scale-150/0/800')}>Enlarging</div>
<div style={tw('loop-scale-110/1000')}>Breathing</div>
<div style={tw('exit-scale-50/2000/500')}>Shrinking</div>
Rotate Animations
<div style={tw('enter-rotate-90/0/500')}>Quarter Turn</div>
<div style={tw('enter-rotate-180/0/1000')}>Half Turn</div>
<div style={tw('loop-rotate-360/2000')}>Spinning</div>
<div style={tw('enter--rotate-45/0/500')}>Counter Rotation</div>
Skew Animations
<div style={tw('enter-skew-x-12/0/500')}>Slanted</div>
<div style={tw('enter-skew-y-6/0/800')}>Tilted</div>
<div style={tw('loop-skew-x-6/1000')}>Wobbling</div>
<div style={tw('exit--skew-x-12/2000/500')}>Reverse Slant</div>
Combining Utilities
You can combine multiple utility animations on the same element:
<div style={tw('enter-translate-y-10/0/500 enter-rotate-45/0/500')}>
Flying In
</div>
<div style={tw('enter-opacity-100/0/800 enter-scale-100/0/800')}>
Appearing
</div>
<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:
<h1 style={tw('enter-slide-up/[0.6s]/[1.5s]')}>Hello</h1>
<h1 style={tw('enter-fade-in/[300ms]/[800ms]')}>World</h1>
<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.
<h1 style={tw('enter-fade-in/0/500')}>Hello</h1>
<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.
<div style={tw('enter-slide-left/0/500')}>Content</div>
<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.
<h1 style={tw('enter-bounce-in/0/500')}>Bouncy!</h1>
<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.
<div style={tw('enter-scale-in/0/500')}>Growing</div>
<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.
<div style={tw('enter-rotate-in/0/500')}>Spinning</div>
<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.
<h1 style={tw('exit-fade-out/2500/500')}>Goodbye</h1>
<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).
<div style={tw('loop-fade/500')}>Pulsing</div>
<div style={tw('loop-bounce/800')}>Bouncing</div>
<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.
<h1 style={tw('ease-in enter-fade-in/0/1000')}>Accelerating</h1>
<h1 style={tw('ease-out enter-fade-in/0/1000')}>Decelerating</h1>
<h1 style={tw('ease-in-out enter-fade-in/0/1000')}>Smooth</h1>
<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.
<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>
<div style={tw('enter-ease-out enter-bounce-in/0/400 loop-ease-linear loop-fade/1000')}>
Bouncy entrance, linear loop
</div>
<div style={tw('ease-in-out enter-fade-in/0/500 exit-fade-out/2500/500')}>
Same easing for both
</div>
<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 |
Spring Easing
Spring easing creates natural, physics-based bouncy animations. Use the built-in ease-spring easing or create custom springs with configurable parameters.
<h1 style={tw('ease-spring enter-bounce-in/0/500')}>Bouncy spring!</h1>
<div style={tw('enter-ease-spring enter-fade-in/0/500 exit-ease-out exit-fade-out/2500/500')}>
Spring entrance, smooth exit
</div>
<h1 style={tw('ease-spring/1/100/10 enter-scale-in/0/800')}>
Custom spring (mass=1, stiffness=100, damping=10)
</h1>
<div style={tw('ease-spring/1/170/8 enter-bounce-in-up/0/600')}>
Extra bouncy!
</div>
<div style={tw('ease-spring/1// //')}>
Snappy spring
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:
ease-spring/1/100/10
ease-spring/1/170/8
ease-spring/1/200/15
ease-spring/2/100/8
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}
</>
);
}
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 }) {
const hue = (frame * 5) % 360;
const fps = 30;
const pulse = Math.sin(frame / fps * Math.PI * 2) * 0.2 + 0.8;
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 }) {
const opacity = progress < 0.3 ? progress / 0.3 : 1;
const scale = 0.8 + progress * 0.2;
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 }) {
const eased = progress * progress * (3 - 2 * progress);
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 }) {
const rocket = path.followQuadratic(
{ x: 200, y: 400 },
{ x: 960, y: 150 },
{ x: 1720, y: 400 },
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"
/>
{/* Element following the path */}
🚀
);
}
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 }) {
const pathFollower = (t) => {
const visibleProgress = progress * 1.5;
const opacity = t < visibleProgress ? 1 : 0;
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 = = , || }}>
{char}
))}
);
}
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/${ * }/`)
}}
>
{char.props.children}
))}
);
}
Loop animation with text on curve:
export default function LoopingCurveText({ tw, textPath, 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
progress for 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
frame for 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):
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 };
}
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):
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 };
}
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 - * mt * t * p1. - * t2 * p2. + * mt * t * p2. + * t2 * p3.;
.(dy, dx) * ( / .);
}
Circle:
function pointOnCircle(cx, cy, radius, angleRadians) {
return {
x: cx + radius * Math.cos(angleRadians),
y: cy + radius * Math.sin(angleRadians)
};
}
const angleRadians = progress * Math.PI * 2;
const pos = pointOnCircle(300, 300, 100, angleRadians);
const tangentAngle = (angleRadians * 180 / Math.PI) + 90;
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%:
<path style={tw('enter-stroke-dash-[300]/0/1000')} />
<path style={tw('ease-spring enter-stroke-dash-[500]/0/1500')} />
<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%:
<path style={tw('exit-stroke-dash-[300]/2000/500')} />
<path style={tw('enter-stroke-dash-[400]/0/800 exit-stroke-dash-[400]/2200/800')} />
Loop Animations
Continuously draw and erase:
<path style={tw('loop-stroke-dash-[300]/2000')} />
<path style={tw('loop-stroke-dash-[200]/1000')} />
Getting Path Length
To find the path length for your SVG:
const path = document.querySelector('path');
const length = path.getTotalLength();
console.log(length);
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"
=
=
=' []//')}
/>
);
}
Combining with Other Animations
Stroke animations work alongside other animation classes:
<path style={tw('enter-stroke-dash-[300]/0/1000 enter-fade-in/0/1000')} />
<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 }) {
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"
=
=
=
=
=
= * }
/>
);
}
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
- 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
Next Steps
- Templates - Creating image and video templates
- Helpers - QR codes, images, and more
Template Helpers
Additional helpers for creating powerful, composable templates.
Overview
Beyond the basics, loopwind provides:
template() - Compose templates together
qr() - Generate QR codes on the fly
config - Access user configuration
For image embedding, see the Images page.
Template Composition
Compose multiple templates together to create complex designs.
Usage
export default function CompositeCard({ tw, template, title, author, avatar }) {
return (
<div style={tw('w-full h-full bg-gradient-to-br from-purple-600 to-blue-500 p-12')}>
<div style={tw('bg-white rounded-2xl p-8 shadow-xl')}>
<h1 style={tw('text-4xl font-bold text-gray-900 mb-6')}>{title}</h1>
{/* Embed another template */}
<div style={tw('mb-6')}>
{template('user-badge', {
name: author,
avatar: avatar
})}
</div>
<p style={tw('text-gray-600')}>Published by {author}</p>
</div>
</div>
);
}
How it works:
template(name, props) renders another installed template
- The embedded template is rendered at its specified size
- You can embed multiple templates in one design
- Templates can be nested (template within a template)
Use Cases
1. Reusable components:
<div>{template('company-logo', { variant: 'dark' })}</div>
2. Complex layouts:
<div style={tw('grid grid-cols-2 gap-4')}>
{template('product-card', { product: product1 })}
{template('product-card', { product: product2 })}
</div>
3. Dynamic content:
{users.map(user =>
template('user-avatar', { name: user.name, image: user.avatar })
)}
Best Practices
- Keep templates focused - Each template should do one thing well
- Pass minimal props - Only pass what the embedded template needs
- Document dependencies - Note which templates are required in your README
- Avoid deep nesting - Too many nested templates can be hard to debug
QR Codes
Generate QR codes dynamically in your templates.
Usage
export default function QRCard({ tw, qr, title, url }) {
return (
<div style={tw('flex flex-col items-center justify-center w-full h-full bg-white p-10')}>
<h1 style={tw('text-4xl font-bold text-black mb-8')}>{title}</h1>
{/* Generate QR code for the URL */}
<img src={qr(url)} style={tw('w-64 h-64')} />
<p style={tw('text-gray-600 mt-4')}>{url}</p>
</div>
);
}
Props format:
{
"title": "Scan Me",
"url": "https://example.com"
}
QR Options
You can customize QR code appearance:
<img src={qr('https://example.com')} />
<img src={qr('https://example.com', { errorCorrectionLevel: 'H' })} />
<img src={qr('https://example.com', { width: 512 })} />
Error correction levels:
L - Low (~7% correction)
M - Medium (~15% correction) - default
Q - Quartile (~25% correction)
H - High (~30% correction)
User Configuration
Access user settings from .loopwind/loopwind.json using the config prop:
export default function BrandedTemplate({ tw, config, title }) {
const primaryColor = config?.colors?.brand || '#6366f1';
return (
<div style={tw('w-full h-full p-12')}>
<h1 style={{
...tw('text-6xl font-bold'),
color: primaryColor
}}>
{title}
</h1>
</div>
);
}
User's .loopwind/loopwind.json:
{
"colors": {
"brand": "#ff6b6b"
},
"fonts": {
"sans": ["Inter", "system-ui", "sans-serif"]
}
}
This allows templates to adapt to user preferences and brand guidelines.
Text on Path
Render text along curves, circles, and custom paths with automatic character positioning and rotation.
Usage
export default function CircleText({ tw, textPath, message }) {
return (
<div style={tw('relative w-full h-full bg-slate-900')}>
{textPath.onCircle(
message,
960, // center x
540, // center y
400, // radius
0, // rotation offset (0-1)
{
fontSize: "4xl",
fontWeight: "bold",
color: "white",
letterSpacing: 0.05
}
)}
</div>
);
}
Available Functions
All textPath functions return an array of positioned character elements:
textPath.onCircle(text, cx, cy, radius, offset, options?)
textPath.onCircle("HELLO WORLD", 960, 540, 400, 0, {
fontSize: "4xl",
color: "white"
})
textPath.onPath(text, pathFollower, options?)
textPath.onPath("CUSTOM PATH", (t) => ({
x: 100 + t * 800,
y: 200 + Math.sin(t * Math.PI) * 100,
angle: Math.cos(t * Math.PI) * 20
}), {
fontSize: "2xl",
fontWeight: "semibold"
})
textPath.onQuadratic(text, p0, p1, p2, options?)
textPath.onQuadratic(
"CURVED TEXT",
{ x: 200, y: 400 },
{ x: 960, y: 100 },
{ x: 1720, y: 400 },
{ fontSize: "3xl", color: "blue-300" }
)
textPath.onCubic(text, p0, p1, p2, p3, options?)
textPath.onCubic(
"S-CURVE",
{ x: 200, y: 600 },
{ x: 600, y: 400 },
{ x: 1320, y: 800 },
{ x: 1720, y: 600 },
{ fontSize: "3xl", color: "purple-300" }
)
textPath.onArc(text, cx, cy, radius, startAngle, endAngle, options?)
textPath.onArc(
"ARC TEXT",
960,
540,
400,
0,
180,
{ fontSize: "2xl", color: "pink-300" }
)
Options
All textPath functions accept an optional options object:
{
fontSize?: string;
fontWeight?: string;
color?: string;
letterSpacing?: number;
style?: any;
}
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>
);
}
Text following a drawn path:
export default function PathText({ tw, textPath }) {
return (
<div style={tw('relative w-full h-full bg-gray-900')}>
{/* Draw the path */}
<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>
{/* Text following the path */}
{textPath.onQuadratic(
"FOLLOWING THE CURVE",
{ x: 200, y: 400 },
{ x: 960, y: 150 },
{ x: 1720, y: 400 },
{ fontSize: "3xl", fontWeight: "bold", color: "blue-300" }
)}
</div>
);
}
For animated text paths, see Text Path Animations.
Reserved Prop Names
The following prop names are reserved and cannot be used in your template's meta.props:
tw, qr, image, template - Core helpers
path, textPath - Path and text helpers
config, frame, progress - System props
Why? These names are used for loopwind's built-in helpers. Using them as prop names would cause conflicts.
Example:
export const meta = {
props: {
title: "string",
image: "string"
}
};
export const meta = {
props: {
title: "string",
imageUrl: "string",
logoUrl: "string"
}
};
If you try to use a reserved name, you'll get a helpful error:
Template uses reserved prop names: image
Try renaming: "image" → "imageUrl" or "imageSrc"
Reserved names: tw, qr, image, template, path, textPath, config, frame, progress
All Props Reference
Every template receives these props:
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
}) {
}
Next Steps
Styling Templates
Style your templates with Tailwind utility classes and shadcn/ui's beautiful design system.
Quick Start
export default function MyTemplate({ title, tw }) {
return (
<div style={tw('flex items-center justify-center w-full h-full bg-gradient-to-br from-blue-600 to-purple-700')}>
<h1 style={tw('text-7xl font-bold text-white')}>
{title}
</h1>
</div>
);
}
The tw() Function
Every template receives a tw() function that converts Tailwind classes to inline styles compatible with Satori:
tw('flex items-center justify-center p-8 bg-blue-500')
{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '2rem',
backgroundColor: '#3b82f6'
}
Basic Usage
export default function Banner({ title, subtitle, tw }) {
return (
<div style={tw('w-full h-full p-12 bg-gray-50')}>
<h1 style={tw('text-6xl font-bold text-gray-900 mb-4')}>
{title}
</h1>
<p style={tw('text-2xl text-gray-600')}>
{subtitle}
</p>
</div>
);
}
Combining with Custom Styles
Mix Tailwind classes with custom styles using the spread operator:
export default function CustomGradient({ title, tw }) {
return (
<div
style={{
...tw('flex flex-col items-center justify-center w-full h-full p-20'),
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
}}
>
<h1 style={tw('text-8xl font-bold text-white')}>{title}</h1>
</div>
);
}
shadcn/ui Design System
loopwind uses shadcn/ui's design system by default, providing semantic color tokens for beautiful, consistent designs.
Default Color Palette
All templates automatically have access to these semantic colors defined in .loopwind/loopwind.json:
colors: {
primary: '#18181b',
'primary-foreground': '#fafafa',
secondary: '#f4f4f5',
'secondary-foreground': '#18181b',
background: '#ffffff',
foreground: '#09090b',
muted: '#f4f4f5',
'muted-foreground': '#71717a',
accent: '#f4f4f5',
'accent-foreground': '#18181b',
destructive: '#ef4444',
'destructive-foreground': '#fafafa',
border: '#e4e4e7',
input: '#e4e4e7',
ring: '#18181b',
card: ,
: ,
}
Using Semantic Colors
export default function SemanticCard({ title, description, price, tw }) {
return (
<div style={tw('bg-card border border-border rounded-lg p-6')}>
<h2 style={tw('text-card-foreground text-2xl font-bold mb-2')}>
{title}
</h2>
<p style={tw('text-muted-foreground mb-4')}>
{description}
</p>
<div style={tw('text-primary text-3xl font-bold')}>
${price}
</div>
</div>
);
}
Opacity Modifiers
Use Tailwind's slash syntax for opacity with any color:
export default function OpacityExample({ tw }) {
return (
<div style={tw('bg-primary/50')}> {/* 50% opacity */}
<p style={tw('text-muted-foreground/75')}> {/* 75% opacity */}
Subtle text
</p>
<div style={tw('border border-border/30')}> {/* 30% opacity */}
Faint border
</div>
</div>
);
}
Supported syntax:
bg-{color}/{opacity} - Background with opacity
text-{color}/{opacity} - Text with opacity
border-{color}/{opacity} - Border with opacity
Text Hierarchy
tw('text-foreground')
tw('text-muted-foreground')
tw('text-primary')
tw('text-destructive')
Backgrounds
tw('bg-background')
tw('bg-card')
tw('bg-muted')
tw('bg-accent')
Supported Tailwind Classes
Layout
- Display:
flex, inline-flex, block, inline-block, hidden
- Flex Direction:
flex-row, flex-col, flex-row-reverse, flex-col-reverse
- Justify:
justify-start, justify-end, justify-center, justify-between, justify-around
- Align:
items-start, items-end, items-center, items-baseline, items-stretch
Spacing
- Padding:
p-{n}, px-{n}, py-{n}, pt-{n}, pb-{n}, pl-{n}, pr-{n}
- Margin:
m-{n}, mx-{n}, my-{n}, mt-{n}, mb-{n}, ml-{n}, mr-{n}
- Gap:
gap-{n}, gap-x-{n}, gap-y-{n}
- Sizes: 0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 56, 64
Examples:
tw('p-4')
tw('px-8')
tw('m-6')
tw('gap-4')
Sizing
- Width:
w-{n}, w-full, w-screen, w-1/2, w-1/3, w-2/3
- Height:
h-{n}, h-full, h-screen
Examples:
tw('w-full')
tw('h-64')
tw('w-1/2')
Typography
- Font Size:
text-xs, text-sm, text-base, text-lg, text-xl, text-2xl, text-3xl, text-4xl, text-5xl, text-6xl, text-7xl, text-8xl, text-9xl
- Font Weight:
font-thin, font-light, font-normal, font-medium, font-semibold, font-bold, font-extrabold, font-black
- Text Align:
text-left, text-center, text-right
- Line Height:
leading-none, leading-tight, leading-normal, leading-relaxed, leading-loose
Colors
All standard Tailwind colors plus shadcn semantic colors:
Standard colors:
text-{color}-{shade}, bg-{color}-{shade}, border-{color}-{shade}
- Colors:
red, blue, green, yellow, purple, pink, gray, indigo, teal, orange
- Shades:
50, 100, 200, 300, 400, 500, 600, 700, 800, 900
shadcn semantic colors:
text-foreground, text-primary, text-muted-foreground, text-destructive
bg-background, bg-card, bg-muted, bg-accent, bg-primary
border-border, border-input
tw('text-blue-500')
tw('bg-purple-600')
tw('text-primary')
tw('bg-card')
Position & Layout
- Position:
relative, absolute, fixed, sticky
- Inset:
inset-0, top-0, bottom-0, left-0, right-0
- Z-Index:
z-0, z-10, z-20, z-30, z-40, z-50
Borders
- Border Width:
border, border-{n}, border-t, border-b, border-l, border-r
- Border Radius:
rounded, rounded-sm, rounded-md, rounded-lg, rounded-xl, rounded-2xl, rounded-3xl, rounded-full
- Border Color:
border-{color}-{shade}, border-border, border-input
Effects
- Shadow:
shadow-sm, shadow, shadow-md, shadow-lg, shadow-xl, shadow-2xl
- Opacity:
opacity-0, opacity-25, opacity-50, opacity-75, opacity-100
Filters
- Blur:
blur-none, blur-sm, blur, blur-md, blur-lg, blur-xl
- Brightness:
brightness-0, brightness-50, brightness-100, brightness-150, brightness-200
- Contrast:
contrast-0, contrast-50, contrast-100, contrast-150, contrast-200
Gradients
Linear Gradients
tw('bg-gradient-to-r')
tw('bg-gradient-to-br')
tw('bg-gradient-to-t')
tw('from-blue-500')
tw('via-purple-500')
tw('to-pink-500')
tw('bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500')
Gradient Examples
export default function GradientCard({ title, tw }) {
return (
<div style={tw('w-full h-full bg-gradient-to-br from-cyan-500 to-blue-600 p-12')}>
<h1 style={tw('text-white text-6xl font-bold')}>
{title}
</h1>
</div>
);
}
Custom Theme Colors
You can override the default shadcn colors or add your own custom colors in .loopwind/loopwind.json:
{
"theme": {
"colors": {
"primary": "#3b82f6",
"primary-foreground": "#ffffff",
"accent": "#10b981",
"brand": "#ff6b6b"
}
}
}
Then use these custom colors in your templates:
tw('text-brand')
tw('bg-primary')
tw('bg-accent')
Auto-Detection from tailwind.config.js
loopwind automatically detects and loads your project's Tailwind configuration:
your-project/
├── tailwind.config.js ← Automatically detected
└── .loopwind/
├── loopwind.json
└── templates/
This includes:
- Custom colors
- Custom spacing values
- Custom fonts
- Theme extensions
- Custom utilities
Complete Example
export default function ModernCard({
tw,
image,
title,
description,
category,
author,
avatar
}) {
return (
<div style={tw('w-full h-full bg-card')}>
{/* Hero image */}
<div style={tw('relative h-2/3')}>
<img
src={image(hero)}
style={tw('w-full h-full object-cover')}
/>
{/* Category badge */}
<div style={tw('absolute top-4 left-4 bg-primary/90 backdrop-blur px-4 py-2 rounded-full')}>
<span style={tw('text-sm font-semibold text-primary-foreground')}>
{category}
</span>
</div>
</div>
{/* Content */}
<div ='/ ')}>
{title}
{description}
{/* Author */}
{author}
);
}
Why This Approach?
- Semantic naming:
text-primary instead of text-blue-600
- Consistency: All templates use the same design language
- Flexibility: Easy to customize entire theme
- Accessibility: Pre-tested color contrasts
- Modern: Same system as shadcn/ui components
- Familiar: Standard Tailwind syntax
Next Steps
Font Handling in loopwind
The recommended way to use fonts is through loopwind.json - configure fonts once, use everywhere.
Using Fonts from loopwind.json (Recommended)
Configure fonts in your .loopwind/loopwind.json and use Tailwind classes in templates.
Simple Setup
Define font families in .loopwind/loopwind.json without loading custom fonts (uses system fonts):
{
"fonts": {
"sans": ["Inter", "system-ui", "-apple-system", "sans-serif"],
"serif": ["Georgia", "serif"],
"mono": ["Courier New", "monospace"]
}
}
Template usage:
export default function({ title, tw }) {
return (
<div style={tw('w-full h-full')}>
{/* Uses fonts.sans from loopwind.json */}
<h1 style={tw('font-sans text-6xl font-bold')}>
{title}
</h1>
{/* Uses fonts.mono from loopwind.json */}
<code style={tw('font-mono text-sm')}>
{code}
</code>
</div>
);
}
Result: Uses system fonts, falls back to Inter for rendering.
Complete Setup (With Font Files)
Load custom font files for brand-specific typography in .loopwind/loopwind.json:
{
"fonts": {
"sans": {
"family": ["Inter", "system-ui", "sans-serif"],
"files": [
{ "path": "./fonts/Inter-Regular.woff", "weight": 400 },
{ "path": "./fonts/Inter-Bold.woff", "weight": 700 }
]
},
"mono": {
"family": ["JetBrains Mono", "monospace"],
"files": [