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:
# Official templates
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:
# Image with inline props
loopwind render banner-hero '{"title":"Hello World"}'
# Video with inline props
loopwind render video-intro '{"title":"Welcome"}'
# Using a props file
loopwind render banner-hero props.json
# Custom output
loopwind render banner-hero '{"title":"Hello"}' --out custom-name.png
# Different format
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 metaexists 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:
// Fade in: starts at 0ms, lasts 500ms
<h1 style={tw('enter-fade-in/0/500')}>Hello</h1>
// Loop: ping effect every 500ms
<div style={tw('loop-ping/500')} />
// Combined with easing
<h1 style={tw('ease-out enter-bounce-in-up/0/600')}>Title</h1>
See Animation for the complete reference.
Next Steps
- Templates
- Embedding Images
- Animation
- Helpers (QR, Template Composition)
- Styling with Tailwind & shadcn/ui
- Custom Fonts
- AI Agent Integration
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
// .loopwind/banner-hero/template.tsx
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}
</p>
</div>
);
}
Rendering Images
# Render with inline props
loopwind render banner-hero '{"title":"Hello World","subtitle":"Welcome"}'
# Custom output name
loopwind render banner-hero '{"title":"Hello"}' --out custom-name.png
# Different format
loopwind render banner-hero '{"title":"Hello"}' --format jpeg --quality 95
# Use a props file
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
// .loopwind/video-intro/template.tsx
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
# Render with inline props
loopwind render video-intro '{"title":"Welcome!"}' --out intro.mp4
# Faster encoding with FFmpeg
loopwind render video-intro '{"title":"Welcome!"}' --ffmpeg
# Higher quality (lower CRF = better)
loopwind render video-intro '{"title":"Welcome!"}' --crf 18
FPS and Duration
video: { fps: 30, duration: 3 } // 90 frames total
| 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 }) {
// frame: 0, 1, 2, ... 89 (for 3s @ 30fps)
// progress: 0.0 at start, 0.5 at middle, 1.0 at end
}
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:
// Enter animations: enter-{type}/{delay}/{duration}
<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>
// Exit animations: exit-{type}/{start}/{duration}
<div style={tw('exit-fade-out/2500/500')}>Fade out at 2.5s</div>
// Loop animations: loop-{type}/{duration}
<div style={tw('loop-float/1000')}>Continuous floating</div>
<div style={tw('loop-spin/1000')}>Spinning</div>
// Easing
<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}</h1>
<p style={tw('text-xl text-gray-600')}>{description}</p>
</div>
<p style={tw('text-gray-400')}>yoursite.com</p>
</div>
</div>
);
}
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 ease-out enter-fade-in-up/300/400')}>
{subtitle}
</p>
</div>
);
}
Next Steps
- Layouts - Wrap templates with reusable layouts
- Embedding Images - Using the
image()helper - Animation - Full animation reference
- Styling - Tailwind & shadcn/ui integration
- Fonts - Custom fonts
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:
// .loopwind/base-layout/template.tsx
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 */}
<div style={tw('flex items-center justify-between px-8 py-4 border-t border-border')}>
<span style={tw('text-muted-foreground')}>loopwind.dev</span>
</div>
</div>
);
}
Usage in Templates
Reference the layout using a relative path:
// .loopwind/blog-post/template.tsx
export const meta = {
name: 'blog-post',
type: 'image',
layout: '../base-layout', // Layout controls size
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' // Sibling directory
layout: './shared/layout' // Subdirectory
layout: '../../layouts/main' // Parent's sibling
Props Flow
The layout receives:
- All standard helpers (
tw,image,qr,template, etc.) childrenprop containing the rendered child content- Animation context (
frame,progress) for video layouts
export default function Layout({ tw, children, frame, progress }) {
// tw, image, qr, template, path, textPath all available
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:
// .loopwind/video-layout/template.tsx
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 style={tw('flex flex-1')}>
{children}
</div>
{/* Animated footer */}
<div style={tw('flex px-12 py-6 ease-out enter-fade-in/500/400')}>
<span style={tw('text-muted-foreground')}>loopwind.dev</span>
</div>
</div>
);
}
Example: Consistent OG Images
Create a layout for all your OG images:
// .loopwind/og-layout/template.tsx
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</span>
</div>
{/* Slot for page-specific content */}
<div style={tw('flex flex-1 items-center')}>
{children}
</div>
{/* Domain */}
<span style={tw('text-muted-foreground mt-auto')}>mybrand.com</span>
</div>
</div>
);
}
Then create page-specific templates:
// .loopwind/og-blog/template.tsx
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 }) {
// Use fallback if no background prop provided
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={tw('text-6xl font-bold text-white')}>{title}</h1>
</div>
</div>
);
}
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:
// Slide in 20px from the left
<div style={tw('enter-translate-x-5/0/1000')}>Content</div>
// Rotate 90 degrees on entrance
<div style={tw('enter-rotate-90/0/500')}>Spinning</div>
// Fade to 50% opacity in a loop
<div style={tw('loop-opacity-50/1000')}>Pulsing</div>
// Scale down with negative value
<div style={tw('enter--scale-50/0/800')}>Shrinking</div>
Supported Utilities
| Utility | Format | Description | Example |
|---|---|---|---|
| translate-x | enter-translate-x-{value} |
Translate horizontally | enter-translate-x-5 = 20pxenter-translate-x-full = 100%enter-translate-x-[20px] = 20px |
| translate-y | enter-translate-y-{value} |
Translate vertically | loop-translate-y-10 = 40pxenter-translate-y-1/2 = 50%enter-translate-y-[5rem] = 80px |
| opacity | enter-opacity-{n} |
Set opacity (0-100) | enter-opacity-50 = 50% |
| scale | enter-scale-{n} |
Scale element (0-200) | enter-scale-100 = 1.0x |
| rotate | enter-rotate-{n} |
Rotate in degrees | enter-rotate-45 = 45° |
| skew-x | enter-skew-x-{n} |
Skew on X axis in degrees | enter-skew-x-12 = 12° |
| skew-y | enter-skew-y-{n} |
Skew on Y axis in degrees | exit-skew-y-6 = 6° |
Translate value formats:
- Numeric:
5= 20px (Tailwind spacing scale: 1 unit = 4px) - Keywords:
full= 100% - Fractions:
1/2= 50%,1/3= 33.333%,2/3= 66.666%, etc. - Arbitrary values:
[20px],[5rem],[10%](rem converts to px: 1rem = 16px)
All utilities work with:
- All prefixes:
enter-,exit-,loop-,animate- - Negative values: Prefix with
-(e.g.,-translate-x-5,-rotate-45) - Timing syntax: Add
/start/duration(e.g.,enter-translate-x-5/0/800)
Translate Animations
// Numeric (Tailwind spacing): 20px (5 * 4px)
<div style={tw('enter-translate-x-5/0/500')}>Content</div>
// Keyword: Full width (100%)
<div style={tw('enter-translate-y-full/0/800')}>Dropping full height</div>
// Fraction: Half width (50%)
<div style={tw('enter-translate-x-1/2/0/600')}>Slide in halfway</div>
// Arbitrary values: Exact px or rem
<div style={tw('enter-translate-y-[20px]/0/500')}>Slide 20px</div>
<div style={tw('enter-translate-x-[5rem]/0/800')}>Slide 5rem (80px)</div>
// Loop with fractions
<div style={tw('loop-translate-y-1/4/1000')}>Oscillate 25%</div>
// Negative values
<div style={tw('exit--translate-y-8/2000/500')}>Rising</div>
Opacity Animations
// Fade to 100% opacity
<div style={tw('enter-opacity-100/0/500')}>Fading In</div>
// Fade to 50% opacity
<div style={tw('enter-opacity-50/0/800')}>Half Opacity</div>
// Pulse between 50% and 100%
<div style={tw('loop-opacity-50/1000')}>Pulsing</div>
// Fade out to 0%
<div style={tw('exit-opacity-0/2500/500')}>Vanishing</div>
Scale Animations
// Scale from 0 to 100% (1.0x)
<div style={tw('enter-scale-100/0/500')}>Growing</div>
// Scale to 150% (1.5x)
<div style={tw('enter-scale-150/0/800')}>Enlarging</div>
// Pulse scale in a loop
<div style={tw('loop-scale-110/1000')}>Breathing</div>
// Scale down to 50%
<div style={tw('exit-scale-50/2000/500')}>Shrinking</div>
Rotate Animations
// Rotate 90 degrees
<div style={tw('enter-rotate-90/0/500')}>Quarter Turn</div>
// Rotate 180 degrees
<div style={tw('enter-rotate-180/0/1000')}>Half Turn</div>
// Continuous rotation in loop (360 degrees per cycle)
<div style={tw('loop-rotate-360/2000')}>Spinning</div>
// Rotate backwards with negative value
<div style={tw('enter--rotate-45/0/500')}>Counter Rotation</div>
Skew Animations
// Skew on X axis
<div style={tw('enter-skew-x-12/0/500')}>Slanted</div>
// Skew on Y axis
<div style={tw('enter-skew-y-6/0/800')}>Tilted</div>
// Oscillating skew in loop
<div style={tw('loop-skew-x-6/1000')}>Wobbling</div>
// Negative skew
<div style={tw('exit--skew-x-12/2000/500')}>Reverse Slant</div>
Combining Utilities
You can combine multiple utility animations on the same element:
// Translate and rotate together
<div style={tw('enter-translate-y-10/0/500 enter-rotate-45/0/500')}>
Flying In
</div>
// Fade and scale
<div style={tw('enter-opacity-100/0/800 enter-scale-100/0/800')}>
Appearing
</div>
// Enter with translate, exit with rotation
<div style={tw('enter-translate-x-5/0/500 exit-rotate-180/2500/500')}>
Slide and Spin
</div>
Bracket Notation
For more CSS-like syntax, you can use brackets with units:
// Using bracket notation with seconds
<h1 style={tw('enter-slide-up/[0.6s]/[1.5s]')}>Hello</h1>
// Using bracket notation with milliseconds
<h1 style={tw('enter-fade-in/[300ms]/[800ms]')}>World</h1>
// Mix and match - plain numbers are milliseconds
<h1 style={tw('enter-bounce-in/0/[1.2s]')}>Mixed</h1>
Enter Animations
Format: enter-{type}/{startMs}/{durationMs}
startMs- when the animation begins (milliseconds from start)durationMs- how long the animation lasts
When values are omitted (enter-fade-in), it uses the full video duration.
Fade Animations
Simple opacity transitions with optional direction.
// Fade in from 0ms to 500ms
<h1 style={tw('enter-fade-in/0/500')}>Hello</h1>
// Fade in with upward motion
<h1 style={tw('enter-fade-in-up/0/600')}>Hello</h1>
| Class | Description |
|---|---|
enter-fade-in/0/500 |
Fade in (opacity 0 → 1) |
enter-fade-in-up/0/500 |
Fade in + slide up (30px) |
enter-fade-in-down/0/500 |
Fade in + slide down (30px) |
enter-fade-in-left/0/500 |
Fade in + slide from left (30px) |
enter-fade-in-right/0/500 |
Fade in + slide from right (30px) |
Slide Animations
Larger movement (100px) with fade.
// Slide in from left: starts at 0, lasts 500ms
<div style={tw('enter-slide-left/0/500')}>Content</div>
// Slide up from bottom: starts at 200ms, lasts 600ms
<div style={tw('enter-slide-up/200/600')}>Content</div>
| Class | Description |
|---|---|
enter-slide-left/0/500 |
Slide in from left (100px) |
enter-slide-right/0/500 |
Slide in from right (100px) |
enter-slide-up/0/500 |
Slide in from bottom (100px) |
enter-slide-down/0/500 |
Slide in from top (100px) |
Bounce Animations
Playful entrance with overshoot effect.
// Bounce in with scale overshoot
<h1 style={tw('enter-bounce-in/0/500')}>Bouncy!</h1>
// Bounce in from below
<div style={tw('enter-bounce-in-up/0/600')}>Pop!</div>
| Class | Description |
|---|---|
enter-bounce-in/0/500 |
Bounce in with scale overshoot |
enter-bounce-in-up/0/500 |
Bounce in from below |
enter-bounce-in-down/0/500 |
Bounce in from above |
enter-bounce-in-left/0/500 |
Bounce in from left |
enter-bounce-in-right/0/500 |
Bounce in from right |
Scale & Zoom Animations
Size-based transitions.
// Scale in from 50%
<div style={tw('enter-scale-in/0/500')}>Growing</div>
// Zoom in from 0%
<div style={tw('enter-zoom-in/0/1000')}>Zooming</div>
| Class | Description |
|---|---|
enter-scale-in/0/500 |
Scale up from 50% to 100% |
enter-zoom-in/0/500 |
Zoom in from 0% to 100% |
Rotate & Flip Animations
Rotation-based transitions.
// Rotate in 180 degrees
<div style={tw('enter-rotate-in/0/500')}>Spinning</div>
// 3D flip on X axis
<div style={tw('enter-flip-in-x/0/500')}>Flipping</div>
| Class | Description |
|---|---|
enter-rotate-in/0/500 |
Rotate in from -180° |
enter-flip-in-x/0/500 |
3D flip on horizontal axis |
enter-flip-in-y/0/500 |
3D flip on vertical axis |
Exit Animations
Format: exit-{type}/{startMs}/{durationMs}
startMs- when the exit animation beginsdurationMs- how long the exit animation lasts
Exit animations use the same timing system but animate elements out.
// Fade out starting at 2500ms, lasting 500ms (ends at 3000ms)
<h1 style={tw('exit-fade-out/2500/500')}>Goodbye</h1>
// Combined enter and exit on same element
<h1 style={tw('enter-fade-in/0/500 exit-fade-out/2500/500')}>
Hello and Goodbye
</h1>
| Class | Description |
|---|---|
exit-fade-out/2500/500 |
Fade out (opacity 1 → 0) |
exit-fade-out-up/2500/500 |
Fade out + slide up |
exit-fade-out-down/2500/500 |
Fade out + slide down |
exit-fade-out-left/2500/500 |
Fade out + slide left |
exit-fade-out-right/2500/500 |
Fade out + slide right |
exit-slide-up/2500/500 |
Slide out upward (100px) |
exit-slide-down/2500/500 |
Slide out downward (100px) |
exit-slide-left/2500/500 |
Slide out to left (100px) |
exit-slide-right/2500/500 |
Slide out to right (100px) |
exit-scale-out/2500/500 |
Scale out to 150% |
exit-zoom-out/2500/500 |
Zoom out to 200% |
exit-rotate-out/2500/500 |
Rotate out to 180° |
exit-bounce-out/2500/500 |
Bounce out with scale |
exit-bounce-out-up/2500/500 |
Bounce out upward |
exit-bounce-out-down/2500/500 |
Bounce out downward |
exit-bounce-out-left/2500/500 |
Bounce out to left |
exit-bounce-out-right/2500/500 |
Bounce out to right |
Loop Animations
Format: loop-{type}/{durationMs}
Loop animations repeat every {durationMs} milliseconds:
/1000= 1 second loop/500= 0.5 second loop/2000= 2 second loop
When duration is omitted (loop-bounce), it defaults to 1000ms (1 second).
// Pulse opacity every 500ms
<div style={tw('loop-fade/500')}>Pulsing</div>
// Bounce every 800ms
<div style={tw('loop-bounce/800')}>Bouncing</div>
// Full rotation every 2000ms
<div style={tw('loop-spin/2000')}>Spinning</div>
| Class | Description |
|---|---|
loop-fade/{ms} |
Opacity pulse (0.5 → 1 → 0.5) |
loop-bounce/{ms} |
Bounce up and down |
loop-spin/{ms} |
Full 360° rotation |
loop-ping/{ms} |
Scale up + fade out (radar effect) |
loop-wiggle/{ms} |
Side to side wiggle |
loop-float/{ms} |
Gentle up and down floating |
loop-pulse/{ms} |
Scale pulse (1.0 → 1.05 → 1.0) |
loop-shake/{ms} |
Shake side to side |
Easing Functions
Add an easing class before the animation class to control the timing curve.
// Ease in (accelerate)
<h1 style={tw('ease-in enter-fade-in/0/1000')}>Accelerating</h1>
// Ease out (decelerate) - default
<h1 style={tw('ease-out enter-fade-in/0/1000')}>Decelerating</h1>
// Ease in-out (smooth)
<h1 style={tw('ease-in-out enter-fade-in/0/1000')}>Smooth</h1>
// Strong cubic easing
<h1 style={tw('ease-out-cubic enter-bounce-in/0/500')}>Dramatic</h1>
| Class | Description | Best For |
|---|---|---|
linear |
Constant speed | Mechanical motion |
ease-in |
Slow start, fast end | Exit animations |
ease-out |
Fast start, slow end (default) | Enter animations |
ease-in-out |
Slow start and end | Subtle transitions |
ease-in-cubic |
Strong slow start | Dramatic exits |
ease-out-cubic |
Strong fast start | Impactful entrances |
ease-in-out-cubic |
Strong both ends | Emphasis animations |
ease-in-quart |
Very strong slow start | Powerful exits |
ease-out-quart |
Very strong fast start | Punchy entrances |
ease-in-out-quart |
Very strong both ends | Maximum drama |
Per-Animation-Type Easing
You can apply different easing functions to enter, exit, and loop animations on the same element using enter-ease-*, exit-ease-*, and loop-ease-* classes.
// Different easing for enter and exit
<h1 style={tw('enter-ease-out-cubic enter-fade-in/0/500 exit-ease-in exit-fade-out/2500/500')}>
Smooth entrance, sharp exit
</h1>
// Loop with linear easing, enter with bounce
<div style={tw('enter-ease-out enter-bounce-in/0/400 loop-ease-linear loop-fade/1000')}>
Bouncy entrance, linear loop
</div>
// Default easing still works (applies to all animations)
<div style={tw('ease-in-out enter-fade-in/0/500 exit-fade-out/2500/500')}>
Same easing for both
</div>
// Mix default with specific overrides
<div style={tw('ease-out enter-fade-in/0/500 exit-ease-in-cubic exit-fade-out/2500/500')}>
Default ease-out for enter, cubic-in for exit
</div>
How it works:
- Default easing (
ease-*) applies to ALL animations if no specific override is set - Specific easing (
enter-ease-*,exit-ease-*,loop-ease-*) overrides the default for that animation type - If both are present, specific easing takes priority for its animation type
Available easing classes:
| Default (all animations) | Enter only | Exit only | Loop only |
|---|---|---|---|
ease-in |
enter-ease-in |
exit-ease-in |
loop-ease-in |
ease-out |
enter-ease-out |
exit-ease-out |
loop-ease-out |
ease-in-out |
enter-ease-in-out |
exit-ease-in-out |
loop-ease-in-out |
ease-in-cubic |
enter-ease-in-cubic |
exit-ease-in-cubic |
loop-ease-in-cubic |
ease-out-cubic |
enter-ease-out-cubic |
exit-ease-out-cubic |
loop-ease-out-cubic |
ease-in-out-cubic |
enter-ease-in-out-cubic |
exit-ease-in-out-cubic |
loop-ease-in-out-cubic |
ease-in-quart |
enter-ease-in-quart |
exit-ease-in-quart |
loop-ease-in-quart |
ease-out-quart |
enter-ease-out-quart |
exit-ease-out-quart |
loop-ease-out-quart |
ease-in-out-quart |
enter-ease-in-out-quart |
exit-ease-in-out-quart |
loop-ease-in-out-quart |
linear |
enter-ease-linear |
exit-ease-linear |
loop-ease-linear |
ease-spring |
enter-ease-spring |
exit-ease-spring |
loop-ease-spring |
Spring Easing
Spring easing creates natural, physics-based bouncy animations. Use the built-in ease-spring easing or create custom springs with configurable parameters.
// Default spring easing
<h1 style={tw('ease-spring enter-bounce-in/0/500')}>Bouncy spring!</h1>
// Per-animation-type spring
<div style={tw('enter-ease-spring enter-fade-in/0/500 exit-ease-out exit-fade-out/2500/500')}>
Spring entrance, smooth exit
</div>
// Custom spring with parameters: ease-spring/mass/stiffness/damping
<h1 style={tw('ease-spring/1/100/10 enter-scale-in/0/800')}>
Custom spring (mass=1, stiffness=100, damping=10)
</h1>
// More bouncy spring (lower damping)
<div style={tw('ease-spring/1/170/8 enter-bounce-in-up/0/600')}>
Extra bouncy!
</div>
// Stiffer spring (higher stiffness, faster)
<div style={tw('ease-spring/1/200/12 enter-fade-in-up/0/400')}>
Snappy spring
</div>
// Per-animation-type custom springs
<div style={tw('enter-ease-spring/1/150/10 enter-fade-in/0/500 exit-ease-spring/1/100/15 exit-fade-out/2500/500')}>
Different springs for enter and exit
</div>
Spring parameters:
| Parameter | Description | Effect when increased | Default |
|---|---|---|---|
| mass | Mass of the spring | Slower, more inertia | 1 |
| stiffness | Spring stiffness | Faster, snappier | 100 |
| damping | Damping coefficient | Less bounce, smoother | 10 |
Common spring presets:
// Gentle bounce (default)
ease-spring/1/100/10
// Extra bouncy
ease-spring/1/170/8
// Snappy (no bounce)
ease-spring/1/200/15
// Slow and bouncy
ease-spring/2/100/8
// Fast and tight
ease-spring/0.5/300/20
How spring works:
- Default
ease-spring- Uses a pre-calculated spring curve optimized for most use cases - Custom
ease-spring/mass/stiffness/damping- Generates a physics-based spring curve using the damped harmonic oscillator formula - The spring automatically calculates its ideal duration to reach the final state
- Works with all animation types:
ease-spring,enter-ease-spring,exit-ease-spring,loop-ease-spring
Combining Enter and Exit
You can use both enter and exit animations on the same element:
export default function EnterExit({ tw, title }) {
return (
<div style={tw('flex items-center justify-center w-full h-full bg-black')}>
{/* Fade in during first 500ms, fade out during last 500ms (assuming 3s video) */}
<h1 style={tw('text-8xl font-bold text-white enter-fade-in/0/500 exit-fade-out/2500/500')}>
{title}
</h1>
</div>
);
}
The opacities from multiple animations are multiplied together, so you get smooth transitions that combine properly.
Staggered Animations
Create sequenced animations by offsetting start times:
export default function StaggeredList({ tw, items }) {
return (
<div style={tw('flex flex-col gap-4')}>
{/* First item: starts at 0ms, lasts 300ms */}
<div style={tw('ease-out enter-fade-in-left/0/300')}>
{items[0]}
</div>
{/* Second item: starts at 100ms, lasts 300ms */}
<div style={tw('ease-out enter-fade-in-left/100/300')}>
{items[1]}
</div>
{/* Third item: starts at 200ms, lasts 300ms */}
<div style={tw('ease-out enter-fade-in-left/200/300')}>
{items[2]}
</div>
</div>
);
}
Dynamic Staggering
For dynamic lists, calculate the timing programmatically:
export default function DynamicStagger({ tw, items }) {
return (
<div style={tw('flex flex-col gap-4')}>
{items.map((item, i) => {
const start = i * 100; // Each item starts 100ms later
const duration = 300; // Each animation lasts 300ms
return (
<div
key={i}
style={tw(`ease-out enter-fade-in-up/${start}/${duration}`)}
>
{item}
</div>
);
})}
</div>
);
}
Common Patterns
Intro Sequence
export default function IntroVideo({ tw, title, subtitle, logo }) {
return (
<div style={tw('flex flex-col items-center justify-cen
…(truncated)