creator-plugin-development

Installation
SKILL.md

Creator Plugin Development

Creator Plugins extend the LottieFiles Creator animation application. They have a two-part sandboxed architecture:

  1. Plugin Sandbox (plugin/plugin.ts) — Runs in isolation with access to the creator global API. Can manipulate scenes, layers, shapes, keyframes. Cannot make network requests.
  2. UI (src/) — Standard React application rendered in an iframe. Can make network requests via fetch. Cannot access the creator API.

The two parts communicate exclusively via message passing.

Project Structure

my-plugin/
├── plugin/
│   ├── manifest.json          # Plugin metadata (id, name, apiVersion, entry, ui)
│   ├── plugin.ts              # Sandbox code — has `creator` API access
│   └── [helpers].ts           # Optional helper modules
├── src/
│   ├── main.tsx               # React DOM entry point
│   ├── app.tsx                # Main UI component
│   └── components/            # React components
├── vite.config.ts             # Uses @lottiefiles/vite-plugin-creator
├── tsconfig.json              # Root config with references
├── tsconfig.plugin.json       # Plugin sandbox TypeScript config (no DOM)
├── tsconfig.app.json          # UI TypeScript config (DOM + JSX)
├── index.html                 # Vite app template
└── package.json

tsconfig.plugin.json compiles sandbox code (no DOM libs). tsconfig.app.json compiles UI code (DOM + JSX).

The plugin manifest (plugin/manifest.json) defines the plugin's identity and entry points:

{
  "id": "unique-uuid-v4",
  "name": "My Plugin",
  "apiVersion": "1",
  "entry": "plugin.js",
  "ui": "ui.html"
}

Development Commands

pnpm create-plugin my-plugin      # Scaffold a new plugin (run from repo root)

# From the plugin directory (e.g., plugins/my-plugin/):
pnpm dev                           # Start dev server with HTTPS hot-reload
pnpm build                         # TypeScript check + Vite production build
pnpm exec tsc -b                   # Type check (run before completing any task)

To load in Creator: Plugins > Develop > New plugin > enter the localhost URL from pnpm dev.

Communication Pattern (Critical)

This is the most common source of bugs. The message wrapping is asymmetric:

UI to Plugin

// In UI code (src/app.tsx) — MUST wrap in pluginMessage object
parent.postMessage(
  { pluginMessage: { type: 'create-shape', color: '#ff0000' } },
  '*'
);

Plugin Receives Message

// In plugin sandbox (plugin/plugin.ts) — messages arrive unwrapped
creator.ui.onMessage((msg) => {
  if (msg.type === 'create-shape') {
    // Use creator API here
  }
});

Plugin to UI

// In plugin sandbox — no wrapping needed
creator.ui.postMessage({ type: 'shape-created', layerId: layer.id });

UI Receives Message

// In UI code — messages arrive wrapped in pluginMessage
window.addEventListener('message', (event) => {
  const message = event.data.pluginMessage;
  if (message?.type === 'shape-created') {
    // Handle response
  }
});

Type-Safe Messages

Define shared message types to catch mismatches at compile time:

// shared/types.ts
export type PluginMessage =
  | { type: 'create-shape'; color: string }
  | { type: 'import-svg'; content: string }
  | { type: 'delete-selection' };

For request/response tracking, include a messageId field.

Key API Patterns

Initialize Plugin

creator.ui.show({ width: 300, height: 500 });

Scene Access

const scene = creator.activeScene;
scene.size;        // { width, height }
scene.duration;    // seconds
scene.framerate;   // FPS
scene.layers;      // ReadonlyArray<Layer>

Create Shapes

const layer = creator.activeScene.createShapeLayer();
const rect = layer.createRectangle({ size: { width: 200, height: 150 } });
layer.createFill({ type: 'SOLID', color: { r: 66, g: 133, b: 244 } });

Import Assets

// From URL
const anim = await scene.import({ type: 'LOTTIE', url: 'https://...' });
const img = await scene.import({ type: 'IMAGE', url: 'https://...' });
const svg = await scene.import({ type: 'SVG', url: 'https://...' });

// From content string
const svgLayer = await scene.import({ type: 'SVG', content: svgString });

LOTTIE and SVG imports return SceneLayer. IMAGE imports return ImageLayer.

Animate Properties

layer.position.addKeyframes([
  { frame: 0, value: { x: 100, y: 100 } },
  { frame: 60, value: { x: 400, y: 100 } },
]);

// With easing
const easeInOut = { type: 'CUBIC_BEZIER', x1: 0.42, y1: 0, x2: 0.58, y2: 1 };
layer.position.addKeyframes([
  { frame: 0, value: { x: 50, y: 100 }, easing: easeInOut },
  { frame: 60, value: { x: 350, y: 100 } },
]);

Selection

const selectedNodes = creator.selection.nodes;

creator.on('selection:nodes', (nodes) => {
  creator.ui.postMessage({ type: 'selection-changed', count: nodes.length });
});

Node Type Checking

Always verify node types before operations:

const layers = creator.selection.nodes;
layers.forEach((node) => {
  if (node.type === 'SHAPE_LAYER') {
    // Shape layer operations (has .shapes, .fills, .strokes, .trimPaths)
  } else if (node.type === 'IMAGE_LAYER') {
    // Image layer operations (has .image)
  } else if (node.type === 'SCENE_LAYER') {
    // Scene layer operations (has .scene, .break())
  } else if (node.type === 'TEXT_LAYER') {
    // Text layer operations (has .text)
  }
});

Network Requests

The plugin sandbox cannot make fetch requests. Use this pattern:

  1. UI fetches data from external API
  2. UI sends data to plugin via parent.postMessage({ pluginMessage: ... }, '*')
  3. Plugin processes data and applies to scene

For complete examples, see references/network-and-libraries.md.

Common Pitfalls

  1. Missing pluginMessage wrapper — UI-to-plugin messages MUST be wrapped: { pluginMessage: { ... } }. Plugin-to-UI messages do NOT need wrapping.
  2. Fetching from plugin sandbox — Network requests only work in UI code. Move fetch calls to src/.
  3. Using localStorage/sessionStorage — The sandboxed iframe blocks browser storage APIs. Use creator.clientStorage from plugin code instead.
  4. Not checking node types — Always verify node.type before accessing type-specific properties.
  5. Setting staticValue on animated properties — Setting staticValue when keyframes exist will not affect the animation. Clear keyframes first or modify keyframe values directly.
  6. Invisible shapes — Shapes need a fill or stroke to be visible. After createRectangle(), call createFill().
  7. Scale values are percentages100 = 100% scale (not 1.0). Use { x: 100, y: 100 } for normal size.
  8. Opacity is 0-100 — Not 0-1. Use 100 for fully opaque.
  9. Color values are 0-255 — RGB channels use the range { r: 0-255, g: 0-255, b: 0-255 }.
  10. Not calling creator.ui.show() early — Call it at the top of plugin.ts, before setting up message handlers.
  11. Sending messages before UI is readycreator.ui.postMessage() right after creator.ui.show() will be dropped because the iframe hasn't loaded. Use a "ui-ready" handshake: have the UI send { type: 'ui-ready' } on mount, then send data from the plugin only after receiving that message.

Verification Checklist

Before considering a task complete:

  • Run pnpm exec tsc -b — fix all type errors
  • Test message flow: UI sends > plugin receives > plugin responds > UI receives
  • Confirm network requests are made from UI code, not plugin sandbox
  • Verify pluginMessage wrapping is correct in both directions

Reference Guide

For deeper information, consult these reference files as needed:

Reference When to Consult
references/architecture-and-communication.md Detailed architecture, complete message passing examples, UI API
references/scene-graph-and-nodes.md Scene hierarchy, node types, traversal patterns
references/shapes-styling-animation.md Creating shapes, fills/strokes/gradients, keyframes, easing
references/importing-assets.md LOTTIE/SVG/IMAGE import formats and patterns
references/storage-and-events.md clientStorage, node data, selection events, timeline API
references/network-and-libraries.md Fetch-from-UI pattern, using npm packages
Weekly Installs
1
First Seen
Mar 21, 2026
Installed on
claude-code1