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:

  1. template(name, props) renders another installed template
  2. The embedded template is rendered at its specified size
  3. You can embed multiple templates in one design
  4. Templates can be nested (template within a template)

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

  1. Keep templates focused - Each template should do one thing well
  2. Pass minimal props - Only pass what the embedded template needs
  3. Document dependencies - Note which templates are required in your README
  4. Avoid deep nesting - Too many nested templates can be hard to debug

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