creator-plugin-development
Creator Plugin Development
Creator Plugins extend the LottieFiles Creator animation application. They have a two-part sandboxed architecture:
- Plugin Sandbox (
plugin/plugin.ts) — Runs in isolation with access to thecreatorglobal API. Can manipulate scenes, layers, shapes, keyframes. Cannot make network requests. - UI (
src/) — Standard React application rendered in an iframe. Can make network requests viafetch. Cannot access thecreatorAPI.
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:
- UI fetches data from external API
- UI sends data to plugin via
parent.postMessage({ pluginMessage: ... }, '*') - Plugin processes data and applies to scene
For complete examples, see references/network-and-libraries.md.
Common Pitfalls
- Missing
pluginMessagewrapper — UI-to-plugin messages MUST be wrapped:{ pluginMessage: { ... } }. Plugin-to-UI messages do NOT need wrapping. - Fetching from plugin sandbox — Network requests only work in UI code. Move
fetchcalls tosrc/. - Using
localStorage/sessionStorage— The sandboxed iframe blocks browser storage APIs. Usecreator.clientStoragefrom plugin code instead. - Not checking node types — Always verify
node.typebefore accessing type-specific properties. - Setting
staticValueon animated properties — SettingstaticValuewhen keyframes exist will not affect the animation. Clear keyframes first or modify keyframe values directly. - Invisible shapes — Shapes need a fill or stroke to be visible. After
createRectangle(), callcreateFill(). - Scale values are percentages —
100= 100% scale (not1.0). Use{ x: 100, y: 100 }for normal size. - Opacity is 0-100 — Not 0-1. Use
100for fully opaque. - Color values are 0-255 — RGB channels use the range
{ r: 0-255, g: 0-255, b: 0-255 }. - Not calling
creator.ui.show()early — Call it at the top ofplugin.ts, before setting up message handlers. - Sending messages before UI is ready —
creator.ui.postMessage()right aftercreator.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
pluginMessagewrapping 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 |