← All skills
Tencent SkillHub Β· Developer Tools

Loopwind

Generate images and videos from React + Tailwind CSS templates using the loopwind CLI.

skill openclawclawhub Free
0 Downloads
0 Stars
0 Installs
0 Score
High Signal

Generate images and videos from React + Tailwind CSS templates using the loopwind CLI.

⬇ 0 downloads β˜… 0 stars Unverified but indexed

Install for OpenClaw

Quick setup
  1. Download the package from Yavira.
  2. Extract the archive and review SKILL.md first.
  3. Import or place the package into your OpenClaw setup.

Requirements

Target platform
OpenClaw
Install method
Manual import
Extraction
Extract archive
Prerequisites
OpenClaw
Primary doc
SKILL.md

Package facts

Download mode
Yavira redirect
Package format
ZIP package
Source platform
Tencent SkillHub
What's included
SKILL.md

Validation

  • Use the Yavira download entry.
  • Review SKILL.md after the package is downloaded.
  • Confirm the extracted package contains the expected setup assets.

Install with your agent

Agent handoff

Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.

  1. Download the package from Yavira.
  2. Extract it into a folder your agent can access.
  3. Paste one of the prompts below and point your agent at the extracted folder.
New install

I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete.

Upgrade existing

I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run.

Trust & source

Release facts

Source
Tencent SkillHub
Verification
Indexed source record
Version
0.25.11

Documentation

ClawHub primary doc Primary doc: SKILL.md 173 sections Open source page

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

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 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: // 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.

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

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

FormatBest ForPNG (default)Transparency, sharp text, logosJPEGPhotographs, gradients, smaller filesSVGVector graphics, scalable designs

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 FPSUse Case24Cinematic look, smaller files30Standard web video60Smooth 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

EncoderCommandUse CaseWASM (default)loopwind render ...CI/CD, no dependenciesFFmpegloopwind render ... --ffmpegFaster, 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.

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

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.

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.

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.) children prop 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> ); }

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: TypeFormatDescriptionEnterenter-{type}/{start}/{duration}Animations that play when enteringExitexit-{type}/{start}/{duration}Animations that play when exitingLooploop-{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

UtilityFormatDescriptionExampletranslate-xenter-translate-x-{value}Translate horizontallyenter-translate-x-5 = 20px<br/>enter-translate-x-full = 100%<br/>enter-translate-x-[20px] = 20pxtranslate-yenter-translate-y-{value}Translate verticallyloop-translate-y-10 = 40px<br/>enter-translate-y-1/2 = 50%<br/>enter-translate-y-[5rem] = 80pxopacityenter-opacity-{n}Set opacity (0-100)enter-opacity-50 = 50%scaleenter-scale-{n}Scale element (0-200)enter-scale-100 = 1.0xrotateenter-rotate-{n}Rotate in degreesenter-rotate-45 = 45Β°skew-xenter-skew-x-{n}Skew on X axis in degreesenter-skew-x-12 = 12Β°skew-yenter-skew-y-{n}Skew on Y axis in degreesexit-skew-y-6 = 6Β° Translate value formats: Numeric: 5 = 20px (Tailwind spacing scale: 1 unit = 4px) Keywords: full = 100% Fractions: 1/2 = 50%, 1/3 = 33.333%, 2/3 = 66.666%, etc. Arbitrary values: [20px], [5rem], [10%] (rem converts to px: 1rem = 16px) All utilities work with: All prefixes: enter-, exit-, loop-, animate- Negative values: Prefix with - (e.g., -translate-x-5, -rotate-45) Timing syntax: Add /start/duration (e.g., enter-translate-x-5/0/800)

Translate Animations

// Numeric (Tailwind spacing): 20px (5 * 4px) <div style={tw('enter-translate-x-5/0/500')}>Content</div> // Keyword: Full width (100%) <div style={tw('enter-translate-y-full/0/800')}>Dropping full height</div> // Fraction: Half width (50%) <div style={tw('enter-translate-x-1/2/0/600')}>Slide in halfway</div> // Arbitrary values: Exact px or rem <div style={tw('enter-translate-y-[20px]/0/500')}>Slide 20px</div> <div style={tw('enter-translate-x-[5rem]/0/800')}>Slide 5rem (80px)</div> // Loop with fractions <div style={tw('loop-translate-y-1/4/1000')}>Oscillate 25%</div> // Negative values <div style={tw('exit--translate-y-8/2000/500')}>Rising</div>

Opacity Animations

// Fade to 100% opacity <div style={tw('enter-opacity-100/0/500')}>Fading In</div> // Fade to 50% opacity <div style={tw('enter-opacity-50/0/800')}>Half Opacity</div> // Pulse between 50% and 100% <div style={tw('loop-opacity-50/1000')}>Pulsing</div> // Fade out to 0% <div style={tw('exit-opacity-0/2500/500')}>Vanishing</div>

Scale Animations

// Scale from 0 to 100% (1.0x) <div style={tw('enter-scale-100/0/500')}>Growing</div> // Scale to 150% (1.5x) <div style={tw('enter-scale-150/0/800')}>Enlarging</div> // Pulse scale in a loop <div style={tw('loop-scale-110/1000')}>Breathing</div> // Scale down to 50% <div style={tw('exit-scale-50/2000/500')}>Shrinking</div>

Rotate Animations

// Rotate 90 degrees <div style={tw('enter-rotate-90/0/500')}>Quarter Turn</div> // Rotate 180 degrees <div style={tw('enter-rotate-180/0/1000')}>Half Turn</div> // Continuous rotation in loop (360 degrees per cycle) <div style={tw('loop-rotate-360/2000')}>Spinning</div> // Rotate backwards with negative value <div style={tw('enter--rotate-45/0/500')}>Counter Rotation</div>

Skew Animations

// Skew on X axis <div style={tw('enter-skew-x-12/0/500')}>Slanted</div> // Skew on Y axis <div style={tw('enter-skew-y-6/0/800')}>Tilted</div> // Oscillating skew in loop <div style={tw('loop-skew-x-6/1000')}>Wobbling</div> // Negative skew <div style={tw('exit--skew-x-12/2000/500')}>Reverse Slant</div>

Combining Utilities

You can combine multiple utility animations on the same element: // Translate and rotate together <div style={tw('enter-translate-y-10/0/500 enter-rotate-45/0/500')}> Flying In </div> // Fade and scale <div style={tw('enter-opacity-100/0/800 enter-scale-100/0/800')}> Appearing </div> // Enter with translate, exit with rotation <div style={tw('enter-translate-x-5/0/500 exit-rotate-180/2500/500')}> Slide and Spin </div>

Bracket Notation

For more CSS-like syntax, you can use brackets with units: // Using bracket notation with seconds <h1 style={tw('enter-slide-up/[0.6s]/[1.5s]')}>Hello</h1> // Using bracket notation with milliseconds <h1 style={tw('enter-fade-in/[300ms]/[800ms]')}>World</h1> // Mix and match - plain numbers are milliseconds <h1 style={tw('enter-bounce-in/0/[1.2s]')}>Mixed</h1>

Enter Animations

Format: enter-{type}/{startMs}/{durationMs} startMs - when the animation begins (milliseconds from start) durationMs - how long the animation lasts When values are omitted (enter-fade-in), it uses the full video duration.

Fade Animations

Simple opacity transitions with optional direction. // Fade in from 0ms to 500ms <h1 style={tw('enter-fade-in/0/500')}>Hello</h1> // Fade in with upward motion <h1 style={tw('enter-fade-in-up/0/600')}>Hello</h1> ClassDescriptionenter-fade-in/0/500Fade in (opacity 0 β†’ 1)enter-fade-in-up/0/500Fade in + slide up (30px)enter-fade-in-down/0/500Fade in + slide down (30px)enter-fade-in-left/0/500Fade in + slide from left (30px)enter-fade-in-right/0/500Fade in + slide from right (30px)

Slide Animations

Larger movement (100px) with fade. // Slide in from left: starts at 0, lasts 500ms <div style={tw('enter-slide-left/0/500')}>Content</div> // Slide up from bottom: starts at 200ms, lasts 600ms <div style={tw('enter-slide-up/200/600')}>Content</div> ClassDescriptionenter-slide-left/0/500Slide in from left (100px)enter-slide-right/0/500Slide in from right (100px)enter-slide-up/0/500Slide in from bottom (100px)enter-slide-down/0/500Slide in from top (100px)

Bounce Animations

Playful entrance with overshoot effect. // Bounce in with scale overshoot <h1 style={tw('enter-bounce-in/0/500')}>Bouncy!</h1> // Bounce in from below <div style={tw('enter-bounce-in-up/0/600')}>Pop!</div> ClassDescriptionenter-bounce-in/0/500Bounce in with scale overshootenter-bounce-in-up/0/500Bounce in from belowenter-bounce-in-down/0/500Bounce in from aboveenter-bounce-in-left/0/500Bounce in from leftenter-bounce-in-right/0/500Bounce in from right

Scale & Zoom Animations

Size-based transitions. // Scale in from 50% <div style={tw('enter-scale-in/0/500')}>Growing</div> // Zoom in from 0% <div style={tw('enter-zoom-in/0/1000')}>Zooming</div> ClassDescriptionenter-scale-in/0/500Scale up from 50% to 100%enter-zoom-in/0/500Zoom in from 0% to 100%

Rotate & Flip Animations

Rotation-based transitions. // Rotate in 180 degrees <div style={tw('enter-rotate-in/0/500')}>Spinning</div> // 3D flip on X axis <div style={tw('enter-flip-in-x/0/500')}>Flipping</div> ClassDescriptionenter-rotate-in/0/500Rotate in from -180Β°enter-flip-in-x/0/5003D flip on horizontal axisenter-flip-in-y/0/5003D flip on vertical axis

Exit Animations

Format: exit-{type}/{startMs}/{durationMs} startMs - when the exit animation begins durationMs - how long the exit animation lasts Exit animations use the same timing system but animate elements out. // Fade out starting at 2500ms, lasting 500ms (ends at 3000ms) <h1 style={tw('exit-fade-out/2500/500')}>Goodbye</h1> // Combined enter and exit on same element <h1 style={tw('enter-fade-in/0/500 exit-fade-out/2500/500')}> Hello and Goodbye </h1> ClassDescriptionexit-fade-out/2500/500Fade out (opacity 1 β†’ 0)exit-fade-out-up/2500/500Fade out + slide upexit-fade-out-down/2500/500Fade out + slide downexit-fade-out-left/2500/500Fade out + slide leftexit-fade-out-right/2500/500Fade out + slide rightexit-slide-up/2500/500Slide out upward (100px)exit-slide-down/2500/500Slide out downward (100px)exit-slide-left/2500/500Slide out to left (100px)exit-slide-right/2500/500Slide out to right (100px)exit-scale-out/2500/500Scale out to 150%exit-zoom-out/2500/500Zoom out to 200%exit-rotate-out/2500/500Rotate out to 180Β°exit-bounce-out/2500/500Bounce out with scaleexit-bounce-out-up/2500/500Bounce out upwardexit-bounce-out-down/2500/500Bounce out downwardexit-bounce-out-left/2500/500Bounce out to leftexit-bounce-out-right/2500/500Bounce out to right

Loop Animations

Format: loop-{type}/{durationMs} Loop animations repeat every {durationMs} milliseconds: /1000 = 1 second loop /500 = 0.5 second loop /2000 = 2 second loop When duration is omitted (loop-bounce), it defaults to 1000ms (1 second). // Pulse opacity every 500ms <div style={tw('loop-fade/500')}>Pulsing</div> // Bounce every 800ms <div style={tw('loop-bounce/800')}>Bouncing</div> // Full rotation every 2000ms <div style={tw('loop-spin/2000')}>Spinning</div> ClassDescriptionloop-fade/{ms}Opacity pulse (0.5 β†’ 1 β†’ 0.5)loop-bounce/{ms}Bounce up and downloop-spin/{ms}Full 360Β° rotationloop-ping/{ms}Scale up + fade out (radar effect)loop-wiggle/{ms}Side to side wiggleloop-float/{ms}Gentle up and down floatingloop-pulse/{ms}Scale pulse (1.0 β†’ 1.05 β†’ 1.0)loop-shake/{ms}Shake side to side

Easing Functions

Add an easing class before the animation class to control the timing curve. // Ease in (accelerate) <h1 style={tw('ease-in enter-fade-in/0/1000')}>Accelerating</h1> // Ease out (decelerate) - default <h1 style={tw('ease-out enter-fade-in/0/1000')}>Decelerating</h1> // Ease in-out (smooth) <h1 style={tw('ease-in-out enter-fade-in/0/1000')}>Smooth</h1> // Strong cubic easing <h1 style={tw('ease-out-cubic enter-bounce-in/0/500')}>Dramatic</h1> ClassDescriptionBest ForlinearConstant speedMechanical motionease-inSlow start, fast endExit animationsease-outFast start, slow end (default)Enter animationsease-in-outSlow start and endSubtle transitionsease-in-cubicStrong slow startDramatic exitsease-out-cubicStrong fast startImpactful entrancesease-in-out-cubicStrong both endsEmphasis animationsease-in-quartVery strong slow startPowerful exitsease-out-quartVery strong fast startPunchy entrancesease-in-out-quartVery strong both endsMaximum drama

Per-Animation-Type Easing

You can apply different easing functions to enter, exit, and loop animations on the same element using enter-ease-*, exit-ease-*, and loop-ease-* classes. // Different easing for enter and exit <h1 style={tw('enter-ease-out-cubic enter-fade-in/0/500 exit-ease-in exit-fade-out/2500/500')}> Smooth entrance, sharp exit </h1> // Loop with linear easing, enter with bounce <div style={tw('enter-ease-out enter-bounce-in/0/400 loop-ease-linear loop-fade/1000')}> Bouncy entrance, linear loop </div> // Default easing still works (applies to all animations) <div style={tw('ease-in-out enter-fade-in/0/500 exit-fade-out/2500/500')}> Same easing for both </div> // Mix default with specific overrides <div style={tw('ease-out enter-fade-in/0/500 exit-ease-in-cubic exit-fade-out/2500/500')}> Default ease-out for enter, cubic-in for exit </div> How it works: 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 onlyExit onlyLoop onlyease-inenter-ease-inexit-ease-inloop-ease-inease-outenter-ease-outexit-ease-outloop-ease-outease-in-outenter-ease-in-outexit-ease-in-outloop-ease-in-outease-in-cubicenter-ease-in-cubicexit-ease-in-cubicloop-ease-in-cubicease-out-cubicenter-ease-out-cubicexit-ease-out-cubicloop-ease-out-cubicease-in-out-cubicenter-ease-in-out-cubicexit-ease-in-out-cubicloop-ease-in-out-cubicease-in-quartenter-ease-in-quartexit-ease-in-quartloop-ease-in-quartease-out-quartenter-ease-out-quartexit-ease-out-quartloop-ease-out-quartease-in-out-quartenter-ease-in-out-quartexit-ease-in-out-quartloop-ease-in-out-quartlinearenter-ease-linearexit-ease-linearloop-ease-linearease-springenter-ease-springexit-ease-springloop-ease-spring

Spring Easing

Spring easing creates natural, physics-based bouncy animations. Use the built-in ease-spring easing or create custom springs with configurable parameters. // Default spring easing <h1 style={tw('ease-spring enter-bounce-in/0/500')}>Bouncy spring!</h1> // Per-animation-type spring <div style={tw('enter-ease-spring enter-fade-in/0/500 exit-ease-out exit-fade-out/2500/500')}> Spring entrance, smooth exit </div> // Custom spring with parameters: ease-spring/mass/stiffness/damping <h1 style={tw('ease-spring/1/100/10 enter-scale-in/0/800')}> Custom spring (mass=1, stiffness=100, damping=10) </h1> // More bouncy spring (lower damping) <div style={tw('ease-spring/1/170/8 enter-bounce-in-up/0/600')}> Extra bouncy! </div> // Stiffer spring (higher stiffness, faster) <div style={tw('ease-spring/1/200/12 enter-fade-in-up/0/400')}> Snappy spring </div> // Per-animation-type custom springs <div style={tw('enter-ease-spring/1/150/10 enter-fade-in/0/500 exit-ease-spring/1/100/15 exit-fade-out/2500/500')}> Different springs for enter and exit </div> Spring parameters: ParameterDescriptionEffect when increasedDefaultmassMass of the springSlower, more inertia1stiffnessSpring stiffnessFaster, snappier100dampingDamping coefficientLess bounce, smoother10 Common spring presets: // Gentle bounce (default) ease-spring/1/100/10 // Extra bouncy ease-spring/1/170/8 // Snappy (no bounce) ease-spring/1/200/15 // Slow and bouncy ease-spring/2/100/8 // Fast and tight ease-spring/0.5/300/20 How spring works: 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> ); }

Intro Sequence

export default function IntroVideo({ tw, title, subtitle, logo }) { return ( <div style={tw('flex flex-col items-center justify-center w-full h-full bg-gradient-to-br from-blue-600 to-purple-700')}> {/* Logo appears first */} <img src={logo} style={tw('h-20 mb-8 ease-out enter-scale-in/0/300')} /> {/* Title bounces in */} <h1 style={tw('text-7xl font-bold text-white ease-out enter-bounce-in-up/200/500')}> {title} </h1> {/* Subtitle fades in last */} <p style={tw('text-2xl text-white/80 mt-4 ease-out enter-fade-in-up/400/700')}> {subtitle} </p> </div> ); }

Text Reveal

export default function TextReveal({ tw, words }) { return ( <div style={tw('flex flex-wrap gap-2 justify-center')}> {words.split(' ').map((word, i) => ( <span key={i} style={tw(`text-4xl font-bold ease-out enter-fade-in-up/${i * 100}/200`)} > {word} </span> ))} </div> ); }

Looping Background Element

export default function AnimatedBackground({ tw, children }) { return ( <div style={tw('relative w-full h-full')}> {/* Floating background circles */} <div style={tw('absolute top-10 left-10 w-20 h-20 rounded-full bg-white/10 loop-float/2000')} /> <div style={tw('absolute bottom-20 right-20 w-32 h-32 rounded-full bg-white/10 loop-fade/1500')} /> {/* Main content */} <div style={tw('relative z-10')}> {children} </div> </div> ); }

Full Enter/Exit Animation

export default function FullAnimation({ tw, title }) { return ( <div style={tw('flex items-center justify-center w-full h-full bg-black')}> {/* Enter: starts at 0, lasts 400ms. Exit: starts at 2600ms, lasts 400ms */} <h1 style={tw('text-8xl font-bold text-white ease-out enter-bounce-in-up/0/400 exit-fade-out-up/2600/400')}> {title} </h1> </div> ); }

Programmatic Animations

For complete control beyond animation classes, use progress and frame directly.

Available Props

PropTypeDescriptionprogressnumber0 to 1 through the video (0% to 100%)framenumberCurrent frame number (0, 1, 2, ... totalFrames-1) These are only available in video templates. Use them when animation classes aren't flexible enough.

Using frame

export default function FrameAnimation({ tw, frame, title }) { // Color cycling using frame number const hue = (frame * 5) % 360; // Cycle through colors // Pulsing based on frame const fps = 30; const pulse = Math.sin(frame / fps * Math.PI * 2) * 0.2 + 0.8; // 0.6 to 1.0 return ( <div style={tw('flex items-center justify-center w-full h-full bg-black')}> <h1 style={{ ...tw('text-8xl font-bold'), color: `hsl(${hue}, 70%, 60%)`, transform: `scale(${pulse})` }}> {title} </h1> </div> ); }

Using progress

export default function ProgressAnimation({ tw, progress, title }) { // Custom fade based on progress const opacity = progress < 0.3 ? progress / 0.3 : 1; // Custom scale based on progress const scale = 0.8 + progress * 0.2; // 0.8 to 1.0 return ( <div style={tw('flex items-center justify-center w-full h-full bg-gray-900')}> <h1 style={{ ...tw('text-8xl font-bold text-white'), opacity, transform: `scale(${scale})` }}> {title} </h1> </div> ); }

Custom Easing

export default function CustomEasing({ tw, progress, title }) { // Smoothstep easing const eased = progress * progress * (3 - 2 * progress); // Elastic easing const elastic = Math.pow(2, -10 * progress) * Math.sin((progress - 0.075) * (2 * Math.PI) / 0.3) + 1; return ( <div style={tw('flex items-center justify-center w-full h-full')}> <h1 style={{ ...tw('text-8xl font-bold'), opacity: eased, transform: `translateY(${(1 - elastic) * 100}px)` }}> {title} </h1> </div> ); }

When to Use Programmatic Animations

Use progress/frame instead of animation classes when you need: Custom easing functions (elastic, bounce with specific curves beyond built-in ease-spring) Color cycling or gradients based on time Mathematical animations (sine waves, spirals, etc.) Complex multi-property animations that need precise coordination Conditional logic based on specific frame numbers For everything else, prefer animation classes - they're simpler and more maintainable.

Animating Along Paths

Animate elements along SVG paths with proper rotation using built-in path helpers: export default function PathFollowing({ tw, progress, path }) { // Follow a quadratic Bezier curve - one line! const rocket = path.followQuadratic( { x: 200, y: 400 }, // Start point { x: 960, y: 150 }, // Control point { x: 1720, y: 400 }, // End point progress ); return ( <div style={{ display: 'flex', ...tw('relative w-full h-full bg-gray-900') }}> {/* Draw the path (optional) */} <svg width="1920" height="1080" style={{ position: 'absolute' }}> <path d="M 200 400 Q 960 150 1720 400" stroke="rgba(255,255,255,0.2)" strokeWidth={2} fill="none" /> </svg> {/* Element following the path */} <div style={{ position: "absolute", left: rocket.x, top: rocket.y, transform: `translate(-50%, -50%) rotate(${rocket.angle}deg)`, fontSize: '48px' }} > πŸš€ </div> </div> ); }

Text Path Animations

Combine textPath helpers with animation classes to create animated text along curves: Rotating text around a circle: export default function RotatingCircleText({ tw, textPath, progress }) { return ( <div style={tw('relative w-full h-full bg-black')}> {/* Text rotates around circle using progress */} {textPath.onCircle( "SPINNING TEXT β€’ AROUND β€’ ", 960, // center x 540, // center y 400, // radius progress, // rotation offset (0-1 animates full rotation) { fontSize: "3xl", fontWeight: "bold", color: "yellow-300" } )} </div> ); } Animated text reveal along a path: export default function PathTextReveal({ tw, textPath, progress }) { // Create custom path follower that animates position const pathFollower = (t) => { // Only show characters up to current progress const visibleProgress = progress * 1.5; // Extend range for smooth reveal const opacity = t < visibleProgress ? 1 : 0; // Follow quadratic curve const pos = { x: (1 - t) * (1 - t) * 200 + 2 * (1 - t) * t * 960 + t * t * 1720, y: (1 - t) * (1 - t) * 400 + 2 * (1 - t) * t * 150 + t * t * 400, angle: 0 }; return { ...pos, opacity }; }; return ( <div style={tw('relative w-full h-full bg-gray-900')}> {textPath.onPath( "REVEALING TEXT", pathFollower, { fontSize: "4xl", fontWeight: "bold", color: "blue-300" } ).map((char, i) => ( <div key={i} style={{ ...char.props.style, opacity: char.props.style.opacity || 1 }}> {char} </div> ))} </div> ); } Staggered character entrance: export default function StaggeredCircleText({ tw, textPath }) { const text = "HELLO WORLD"; return ( <div style={tw('relative w-full h-full bg-slate-900')}> {textPath.onCircle( text, 960, 540, 400, 0, { fontSize: "4xl", fontWeight: "bold", color: "white" } ).map((char, i) => { // Stagger fade-in: each character starts 50ms later const staggerDelay = i * 50; return ( <div key={i} style={{ ...char.props.style, ...tw(`enter-fade-in/${staggerDelay}/300 enter-scale-100/${staggerDelay}/300`) }} > {char.props.children} </div> ); })} </div> ); } Text with bounce entrance along arc: export default function BouncyArcText({ tw, textPath }) { return ( <div style={tw('relative w-full h-full bg-gradient-to-br from-purple-600 to-blue-500')}> {/* Draw the arc path */} <svg width="1920" height="1080" style={{ position: 'absolute' }}> <path d="M 300 900 A 600 600 0 0 1 1620 900" stroke="rgba(255,255,255,0.2)" strokeWidth={2} fill="none" strokeDasharray="5 5" /> </svg> {/* Text follows arc with staggered bounce */} {textPath.onArc( "BOUNCING ON ARC", 960, // cx 300, // cy 600, // radius 180, // start angle 360, // end angle { fontSize: "3xl", fontWeight: "bold", color: "white" } ).map((char, i) => ( <div key={i} style={{ ...char.props.style, ...tw(`ease-out enter-bounce-in-up/${i * 80}/500`) }} > {char.props.children} </div> ))} </div> ); } Loop animation with text on curve: export default function LoopingCurveText({ tw, textPath, frame }) { // Calculate wave effect using frame const waveOffset = Math.sin(frame / 30 * Math.PI * 2) * 0.1; return ( <div style={tw('relative w-full h-full bg-black')}> {textPath.onQuadratic( "WAVY TEXT", { x: 200, y: 400 }, { x: 960, y: 150 }, { x: 1720, y: 400 }, { fontSize: "4xl", fontWeight: "bold", color: "pink-300" } ).map((char, i) => ( <div key={i} style={{ ...char.props.style, transform: `${char.props.style.transform} translateY(${Math.sin((i + frame) / 5) * 10}px)` }} > {char.props.children} </div> ))} </div> ); } Tips for animating text paths: 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): // Position: (1-t)Β²Β·P0 + 2(1-t)tΒ·P1 + tΒ²Β·P2 function pointOnQuadraticBezier(p0, p1, p2, t) { const x = (1 - t) * (1 - t) * p0.x + 2 * (1 - t) * t * p1.x + t * t * p2.x; const y = (1 - t) * (1 - t) * p0.y + 2 * (1 - t) * t * p1.y + t * t * p2.y; return { x, y }; } // Tangent angle function angleOnQuadraticBezier(p0, p1, p2, t) { const dx = 2 * (1 - t) * (p1.x - p0.x) + 2 * t * (p2.x - p1.x); const dy = 2 * (1 - t) * (p1.y - p0.y) + 2 * t * (p2.y - p1.y); return Math.atan2(dy, dx) * (180 / Math.PI); } Cubic Bezier (C command): // Position: (1-t)Β³Β·P0 + 3(1-t)Β²tΒ·P1 + 3(1-t)tΒ²Β·P2 + tΒ³Β·P3 function pointOnCubicBezier(p0, p1, p2, p3, t) { const mt = 1 - t; const mt2 = mt * mt; const mt3 = mt2 * mt; const t2 = t * t; const t3 = t2 * t; const x = mt3 * p0.x + 3 * mt2 * t * p1.x + 3 * mt * t2 * p2.x + t3 * p3.x; const y = mt3 * p0.y + 3 * mt2 * t * p1.y + 3 * mt * t2 * p2.y + t3 * p3.y; return { x, y }; } // Tangent angle function angleOnCubicBezier(p0, p1, p2, p3, t) { const mt = 1 - t; const mt2 = mt * mt; const t2 = t * t; const dx = -3 * mt2 * p0.x + 3 * mt2 * p1.x - 6 * mt * t * p1.x - 3 * t2 * p2.x + 6 * mt * t * p2.x + 3 * t2 * p3.x; const dy = -3 * mt2 * p0.y + 3 * mt2 * p1.y - 6 * mt * t * p1.y - 3 * t2 * p2.y + 6 * mt * t * p2.y + 3 * t2 * p3.y; return Math.atan2(dy, dx) * (180 / Math.PI); } Circle: function pointOnCircle(cx, cy, radius, angleRadians) { return { x: cx + radius * Math.cos(angleRadians), y: cy + radius * Math.sin(angleRadians) }; } // Usage const angleRadians = progress * Math.PI * 2; const pos = pointOnCircle(300, 300, 100, angleRadians); const tangentAngle = (angleRadians * 180 / Math.PI) + 90; // Tangent is perpendicular Tips: Use progress (0-1) for smooth animation The translate(-50%, -50%) centers the element on the path Combine rotation with the translate: translate(-50%, -50%) rotate(${angle}deg) For text following a path, you can animate individual characters at different progress values

SVG Stroke Animations

Animate SVG path strokes with the stroke-dash classes, perfect for drawing or erasing line art, icons, and illustrations.

How It Works

SVG stroke animations use strokeDasharray and strokeDashoffset CSS properties to create drawing effects: 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%: // Draw a 300px path over 1 second <path style={tw('enter-stroke-dash-[300]/0/1000')} /> // Draw with spring easing <path style={tw('ease-spring enter-stroke-dash-[500]/0/1500')} /> // Stagger multiple paths <path style={tw('enter-stroke-dash-[200]/0/600')} /> <path style={tw('enter-stroke-dash-[200]/200/600')} /> <path style={tw('enter-stroke-dash-[200]/400/600')} />

Exit Animations (Erasing)

Erase strokes from 100% to 0%: // Erase starting at 2000ms, lasting 500ms <path style={tw('exit-stroke-dash-[300]/2000/500')} /> // Draw then erase the same path <path style={tw('enter-stroke-dash-[400]/0/800 exit-stroke-dash-[400]/2200/800')} />

Loop Animations

Continuously draw and erase: // Loop every 2 seconds (draws in first half, erases in second half) <path style={tw('loop-stroke-dash-[300]/2000')} /> // Faster loop <path style={tw('loop-stroke-dash-[200]/1000')} />

Getting Path Length

To find the path length for your SVG: // In browser console or component: const path = document.querySelector('path'); const length = path.getTotalLength(); console.log(length); // e.g., 347.89 Then use that value: <path style={tw('enter-stroke-dash-[347.89]/0/1000')} />

Complete Example

export default function DrawingEffect({ tw }) { return ( <div style={tw('flex items-center justify-center w-full h-full bg-gray-900')}> <svg width="600" height="400" viewBox="0 0 600 400"> {/* Checkmark icon drawn in sequence */} <path d="M100 200 L 200 300 L 400 100" stroke="#10b981" strokeWidth={8} fill="none" strokeLinecap="round" strokeLinejoin="round" style={tw('ease-out enter-stroke-dash-[600]/0/1200')} /> {/* Circle drawn after checkmark */} <circle cx="250" cy="200" r="150" stroke="#10b981" strokeWidth={6} fill="none" style={tw('ease-out enter-stroke-dash-[942]/1000/1000')} /> </svg> </div> ); }

Combining with Other Animations

Stroke animations work alongside other animation classes: // Fade in while drawing <path style={tw('enter-stroke-dash-[300]/0/1000 enter-fade-in/0/1000')} /> // Draw with pulsing color <svg> <path stroke="url(#gradient)" style={tw('enter-stroke-dash-[500]/0/1500')} /> <defs> <linearGradient id="gradient"> <stop offset="0%" stopColor="#8b5cf6" /> <stop offset="100%" stopColor="#ec4899" /> </linearGradient> </defs> </svg>

Animated Dashed Strokes (Marching Ants)

For marching ants or animated dashed patterns, use frame or progress directly instead of animation classes: export default function MarchingAnts({ tw, frame }) { // Calculate animated offset (loops every 30 frames) const dashOffset = -(frame % 30) * 2; return ( <div style={tw('flex items-center justify-center w-full h-full bg-gray-900')}> <svg width="600" height="400" viewBox="0 0 600 400"> {/* Marching ants border */} <rect x="50" y="50" width="500" height="300" fill="none" stroke="#3b82f6" strokeWidth={3} strokeDasharray="10 5" strokeDashoffset={dashOffset} /> {/* Animated circle with different speed */} <circle cx="300" cy="200" r="80" fill="none" stroke="#10b981" strokeWidth={4} strokeDasharray="15 8" strokeDashoffset={dashOffset * 1.5} /> </svg> </div> ); } Tips: strokeDasharray="10 5" - 10px dash, 5px gap strokeDashoffset={dashOffset} - animates the pattern position Negative offset moves forward, positive moves backward Different speeds: multiply by different values (e.g., dashOffset * 2) This technique is different from stroke-dash classes: stroke-dash classes - Draw/erase the stroke (reveal animation) Marching ants - Move a dashed pattern along the stroke

Performance Tips

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: // Create a logo template once, use it everywhere <div>{template('company-logo', { variant: 'dark' })}</div> 2. Complex layouts: // Combine multiple templates into one design <div style={tw('grid grid-cols-2 gap-4')}> {template('product-card', { product: product1 })} {template('product-card', { product: product2 })} </div> 3. Dynamic content: // Render templates based on data {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: // Basic QR code <img src={qr('https://example.com')} /> // With error correction level <img src={qr('https://example.com', { errorCorrectionLevel: 'H' })} /> // With custom size <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 }) { // Access custom colors from loopwind.json 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?) // Text around a circle textPath.onCircle("HELLO WORLD", 960, 540, 400, 0, { fontSize: "4xl", color: "white" }) textPath.onPath(text, pathFollower, options?) // Text along any custom path 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?) // Text along a quadratic Bezier curve textPath.onQuadratic( "CURVED TEXT", { x: 200, y: 400 }, // start { x: 960, y: 100 }, // control point { x: 1720, y: 400 }, // end { fontSize: "3xl", color: "blue-300" } ) textPath.onCubic(text, p0, p1, p2, p3, options?) // Text along a cubic Bezier curve textPath.onCubic( "S-CURVE", { x: 200, y: 600 }, // start { x: 600, y: 400 }, // control 1 { x: 1320, y: 800 }, // control 2 { x: 1720, y: 600 }, // end { fontSize: "3xl", color: "purple-300" } ) textPath.onArc(text, cx, cy, radius, startAngle, endAngle, options?) // Text along a circular arc textPath.onArc( "ARC TEXT", 960, // center x 540, // center y 400, // radius 0, // start angle (degrees) 180, // end angle (degrees) { fontSize: "2xl", color: "pink-300" } )

Options

All textPath functions accept an optional options object: { fontSize?: string; // Tailwind size: "xl", "2xl", "4xl", etc. fontWeight?: string; // Tailwind weight: "bold", "semibold", etc. color?: string; // Tailwind color: "white", "blue-500", etc. letterSpacing?: number; // Space between characters (0-1, default: 0) style?: any; // Additional inline styles }

Examples

Animated rotating text: export default function RotatingText({ tw, textPath, progress }) { return ( <div style={tw('relative w-full h-full bg-black')}> {textPath.onCircle( "SPINNING β€’ TEXT β€’ ", 960, 540, 400, progress, // Rotate based on video progress { fontSize: "3xl", color: "yellow-300" } )} </div> ); } Multiple text paths: export default function MultiPath({ tw, textPath }) { return ( <div style={tw('relative w-full h-full bg-gradient-to-br from-slate-900 to-slate-700')}> {/* Text on outer circle */} {textPath.onCircle( "OUTER RING", 960, 540, 500, 0, { fontSize: "5xl", fontWeight: "bold", color: "white" } )} {/* Text on inner circle */} {textPath.onCircle( "inner ring", 960, 540, 300, 0.5, // offset by 50% for rotation { fontSize: "2xl", color: "white/60" } )} </div> ); } 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: // ❌ BAD - 'image' is reserved export const meta = { props: { title: "string", image: "string" // Error! } }; // βœ… GOOD - Use descriptive alternatives export const meta = { props: { title: "string", imageUrl: "string", // or imageSrc, photoUrl, etc. 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 }) { // Your template code }

Next Steps

Embedding Images Templates Styling with Tailwind & shadcn/ui Custom Fonts

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: // Tailwind classes tw('flex items-center justify-center p-8 bg-blue-500') // Converts to inline styles: { 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 colors primary: '#18181b', // Main brand color 'primary-foreground': '#fafafa', // Secondary colors secondary: '#f4f4f5', // Subtle accents 'secondary-foreground': '#18181b', // Background background: '#ffffff', // Page background foreground: '#09090b', // Main text color // Muted muted: '#f4f4f5', // Subtle backgrounds 'muted-foreground': '#71717a', // Muted text // Accent accent: '#f4f4f5', // Highlight color 'accent-foreground': '#18181b', // Destructive destructive: '#ef4444', // Error/danger states 'destructive-foreground': '#fafafa', // UI Elements border: '#e4e4e7', // Border color input: '#e4e4e7', // Input borders ring: '#18181b', // Focus rings card: '#ffffff', // Card background 'card-foreground': '#09090b', }

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

// Primary text tw('text-foreground') // Secondary/muted text tw('text-muted-foreground') // Accent/brand text tw('text-primary') // Destructive/error text tw('text-destructive')

Backgrounds

// Page background tw('bg-background') // Card/elevated surfaces tw('bg-card') // Subtle backgrounds tw('bg-muted') // Accent backgrounds tw('bg-accent')

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') // padding: 1rem tw('px-8') // paddingLeft: 2rem, paddingRight: 2rem tw('m-6') // margin: 1.5rem tw('gap-4') // gap: 1rem

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') // width: 100% tw('h-64') // height: 16rem tw('w-1/2') // width: 50%

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') // Standard Tailwind color tw('bg-purple-600') // Standard Tailwind color tw('text-primary') // shadcn semantic color tw('bg-card') // shadcn semantic color

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

Linear Gradients

// Gradient direction tw('bg-gradient-to-r') // left to right tw('bg-gradient-to-br') // top-left to bottom-right tw('bg-gradient-to-t') // bottom to top // Gradient colors tw('from-blue-500') // Start color tw('via-purple-500') // Middle color tw('to-pink-500') // End color // Complete gradient tw('bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500')

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') // Uses your custom brand color tw('bg-primary') // Uses your custom primary color tw('bg-accent') // Uses your custom accent color

Auto-Detection from tailwind.config.js

loopwind automatically detects and loads your project's Tailwind configuration: 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 style={tw('h-1/3 p-8 flex flex-col justify-between')}> <div> <h2 style={tw('text-3xl font-bold text-foreground mb-2')}> {title} </h2> <p style={tw('text-muted-foreground line-clamp-2')}> {description} </p> </div> {/* Author */} <div style={tw('flex items-center gap-3')}> <img src={image(avatar)} style={tw('w-10 h-10 rounded-full border-2 border-border')} /> <span style={tw('text-sm text-muted-foreground')}> {author} </span> </div> </div> </div> ); }

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

Custom Fonts Built-in Helpers Templates Embedding Images

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": [ { "path": "./fonts/JetBrainsMono-Regular.woff", "weight": 400 } ] } } } Project structure: your-project/ β”œβ”€β”€ .loopwind/ β”‚ β”œβ”€β”€ loopwind.json β”‚ └── templates/ └── fonts/ β”œβ”€β”€ Inter-Regular.woff β”œβ”€β”€ Inter-Bold.woff └── JetBrainsMono-Regular.woff Template usage (same as before): <h1 style={tw('font-sans font-bold')}> {/* Uses Inter Bold from loopwind.json */} {title} </h1> Available classes: font-sans - Uses fonts.sans from loopwind.json font-serif - Uses fonts.serif from loopwind.json font-mono - Uses fonts.mono from loopwind.json Supported formats: βœ… WOFF (.woff) - Recommended for best compatibility βœ… TTF (.ttf) - Also supported βœ… OTF (.otf) - Also supported ❌ WOFF2 (.woff2) - Not supported by renderer

Font Loading Priority

loopwind loads fonts in this order: loopwind.json fonts (if configured with files) Bundled Inter fonts (included with CLI) This ensures fonts work out of the box with no configuration.

Default Fonts

If no fonts are configured, loopwind uses Inter (Regular 400, Bold 700) which is bundled with the CLI. This means fonts work offline with no configuration required.

Best Practices

βœ… Use loopwind.json for project-wide fonts - Configure once, use everywhere βœ… Use font classes - tw('font-sans') instead of fontFamily: 'Inter' βœ… Include fallbacks - Always add system fonts: ["Inter", "system-ui", "sans-serif"] βœ… Match names - First font in family array is used as the loaded font name βœ… Relative paths - Font paths are relative to loopwind.json location

Minimal Setup (System Fonts)

{ "fonts": { "sans": ["Inter", "-apple-system", "sans-serif"] } } Uses system Inter if available, falls back to Noto Sans for rendering.

Brand Fonts Setup

{ "fonts": { "sans": { "family": ["Montserrat", "sans-serif"], "files": [ { "path": "./fonts/Montserrat-Regular.woff", "weight": 400 }, { "path": "./fonts/Montserrat-Bold.woff", "weight": 700 } ] } } } Loads and uses Montserrat for all templates.

Multi-Font Setup

{ "fonts": { "sans": { "family": ["Inter", "sans-serif"], "files": [ { "path": "./fonts/Inter-Regular.woff", "weight": 400 }, { "path": "./fonts/Inter-Bold.woff", "weight": 700 } ] }, "serif": { "family": ["Playfair Display", "serif"], "files": [ { "path": "./fonts/Playfair-Regular.woff", "weight": 400 } ] }, "mono": { "family": ["Fira Code", "monospace"], "files": [ { "path": "./fonts/FiraCode-Regular.woff", "weight": 400 } ] } } } Loads different fonts for each style class.

External Font URLs

Load fonts directly from CDNs without downloading files: { "fonts": { "sans": { "family": ["Inter", "sans-serif"], "files": [ { "path": "https://unpkg.com/@fontsource/inter@5.0.18/files/inter-latin-400-normal.woff", "weight": 400 }, { "path": "https://unpkg.com/@fontsource/inter@5.0.18/files/inter-latin-700-normal.woff", "weight": 700 } ] } } } You can also mix local and external fonts: { "fonts": { "sans": { "family": ["Inter", "sans-serif"], "files": [ { "path": "./fonts/Inter-Regular.woff", "weight": 400 }, { "path": "https://unpkg.com/@fontsource/inter@5.0.18/files/inter-latin-700-normal.woff", "weight": 700 } ] } } } Note: Use WOFF format (.woff) for best compatibility. WOFF2 is not supported by the underlying renderer.

Performance

βœ… Font caching - Fonts load once and are cached for all renders βœ… Video optimization - 90-frame video loads fonts once, not 90 times βœ… No CDN delays - Local fonts load instantly

Next Steps

Styling with Tailwind & shadcn/ui Built-in Helpers Templates Embedding Images

loopwind.json

Configure colors and fonts for all your templates in .loopwind/loopwind.json.

File Location

your-project/ β”œβ”€β”€ .loopwind/ β”‚ β”œβ”€β”€ loopwind.json ← Configuration file β”‚ └── templates/

Minimal Example

{ "theme": { "colors": { "primary": "#3b82f6", "background": "#ffffff" } } }

Default shadcn/ui Palette

{ "theme": { "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": "#ffffff", "card-foreground": "#09090b" } } }

Custom Colors

{ "theme": { "colors": { "primary": "#3b82f6", "brand": "#ff6b6b", "success": "#22c55e", "warning": "#f59e0b" } } } Use in templates: tw('text-brand') // #ff6b6b tw('bg-success') // #22c55e tw('border-warning') // #f59e0b

Simple (System Fonts)

{ "fonts": { "sans": ["Inter", "system-ui", "sans-serif"], "serif": ["Georgia", "serif"], "mono": ["Courier New", "monospace"] } }

With Font Files

{ "fonts": { "sans": { "family": ["Inter", "system-ui", "sans-serif"], "files": [ { "path": "./fonts/Inter-Regular.woff", "weight": 400 }, { "path": "./fonts/Inter-Bold.woff", "weight": 700 } ] } } } Paths are relative to loopwind.json. Supported formats: βœ… WOFF (.woff) βœ… TTF (.ttf) βœ… OTF (.otf) ❌ WOFF2 (.woff2)

External URLs

{ "fonts": { "sans": { "family": ["Inter", "sans-serif"], "files": [ { "path": "https://unpkg.com/@fontsource/inter@5.0.18/files/inter-latin-400-normal.woff", "weight": 400 } ] } } }

Complete Example

{ "theme": { "colors": { "primary": "#6366f1", "primary-foreground": "#ffffff", "background": "#ffffff", "foreground": "#0f172a", "muted": "#f1f5f9", "muted-foreground": "#64748b", "border": "#e2e8f0", "card": "#ffffff", "brand": "#8b5cf6" } }, "fonts": { "sans": { "family": ["Inter", "sans-serif"], "files": [ { "path": "./fonts/Inter-Regular.woff", "weight": 400 }, { "path": "./fonts/Inter-Bold.woff", "weight": 700 } ] }, "serif": { "family": ["Playfair Display", "serif"], "files": [ { "path": "./fonts/Playfair-Regular.woff", "weight": 400 } ] } } }

Schema

{ "theme"?: { "colors"?: { [name: string]: string; // Hex color } }, "fonts"?: { [class: string]: string[] | { family: string[]; files: Array<{ path: string; // Local or URL weight: number; // 100-900 }>; } } }

Auto-Detection

If no loopwind.json exists, loopwind auto-detects tailwind.config.js: your-project/ β”œβ”€β”€ tailwind.config.js ← Auto-detected └── .loopwind/ └── templates/ Priority: .loopwind/loopwind.json tailwind.config.js Built-in defaults

Next Steps

Styling - Use colors in templates Fonts - Font configuration details Getting Started - Setup guide

Category context

Code helpers, APIs, CLIs, browser automation, testing, and developer operations.

Source: Tencent SkillHub

Largest current source with strong distribution and engagement signals.

Package contents

Included in package
1 Docs
  • SKILL.md Primary doc