Creator Plugin Development
Creator Plugins extend the LottieFiles Creator animation application. They have a two-part sandboxed architecture:
- Plugin Sandbox (
plugin.ts) — Runs in isolation with access to thecreatorglobal API. Can manipulate scenes, layers, shapes, keyframes. Cannot make network requests. - UI (
ui.htmlorsrc/) — Rendered in an iframe. Can be plain HTML/JS or a React app. Can make network requests viafetch. Cannot access thecreatorAPI.
The two parts communicate exclusively via message passing.
Project Structure
There are two plugin patterns. Identify which one you're working with:
HTML Plugin (simple — most examples use this)
my-plugin/
├── manifest.json # Plugin metadata (id, name, apiVersion, entry, ui)
├── plugin.ts # Sandbox code — has `creator` API access
├── plugin.js # Compiled output (generated by tsc)
├── ui.html # UI — plain HTML/CSS/JS in a single file
└── tsconfig.json # TypeScript config
React Plugin (bundled — for complex UIs)
my-plugin/
├── plugin/
│ ├── manifest.json # Plugin metadata
│ ├── 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
The plugin manifest 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
HTML plugins
npx tsc # Compile plugin.ts → plugin.js
After compiling, load in Creator: Plugins > Develop > New plugin > select the plugin directory.
React plugins
npm install # Install dependencies (first time)
npm run dev # Start dev server with HTTPS hot-reload
npm run build # TypeScript check + Vite production build
npx tsc -b # Type check only
To load in Creator: Plugins > Develop > New plugin > enter the localhost URL from npm run dev.
Communication Pattern (Critical)
This is the most common source of bugs. The message wrapping is asymmetric — and it works the same for both HTML and React plugins.
UI to Plugin
// In UI code (ui.html <script> or src/app.tsx) — MUST wrap in pluginMessage object
parent.postMessage(
{ pluginMessage: { type: 'create-shape', color: '#ff0000' } },
'*'
);
Plugin Receives Message
// In plugin sandbox (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 (React plugins)
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 HTML plugins, define an interface in plugin.ts for the same purpose.
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 (in
ui.htmlscript or React component) - 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 toui.htmlorsrc/. - 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. - Forgetting to recompile HTML plugins — After editing
plugin.ts, runnpx tscto regenerateplugin.js. React plugins auto-rebuild withnpm run dev.
Verification Checklist
Before considering a task complete:
- HTML plugins: Run
npx tsc— fix all type errors - React plugins: Run
npx 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 and CDN libraries |