
Creator Plugin Development
- 1 installs
- 1 repo stars
- Updated March 5, 2026
- lottiefiles/creator-plugin-hackathon-qr-craft
Guides building LottieFiles Creator Plugins with the Creator Plugin API, including scene manipulation and React UI talking to a plugin sandbox.
About
Provides patterns for developing LottieFiles Creator Plugins using the creator global API for scene manipulation and a React UI that communicates with a plugin sandbox. A developer uses it when creating or extending a Creator plugin, importing assets, or animating layers.
- Uses the creator global API for scene manipulation
- Builds plugin UI in React communicating with a sandbox
Creator Plugin Development by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,912 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/lottiefiles/creator-plugin-hackathon-qr-craft --skill creator-plugin-developmentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 1 |
| Last updated | March 5, 2026 |
| Repository | lottiefiles/creator-plugin-hackathon-qr-craft ↗ |
What it does
Guides building LottieFiles Creator Plugins with the Creator Plugin API, including scene manipulation and React UI talking to a plugin sandbox.
Files
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.jsontsconfig.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 percentages — 100 = 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 ready — creator.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
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 |
Architecture and Communication
Plugin Architecture
Creator plugins are separated into two isolated environments for security:
┌─────────────────────────┐ ┌─────────────────────────┐
│ Plugin Sandbox │ │ Plugin UI (iframe) │
│ │ │ │
│ - creator API access │◄───►│ - React/HTML/CSS │
│ - Scene manipulation │ │ - Browser APIs │
│ - NO network access │ │ - fetch() for network │
│ - NO DOM access │ │ - NO creator API access │
│ │ │ │
│ File: plugin/plugin.ts │ │ Files: src/*.tsx │
└─────────────────────────┘ └─────────────────────────┘
▲
│ creator API
▼
┌─────────────────────────┐
│ LottieFiles Creator │
│ (Scene graph) │
└─────────────────────────┘| Plugin Sandbox | UI iframe | |
|---|---|---|
| Runs | Plugin code (plugin.ts) | Interface (App.tsx or HTML) |
| Has access to | Creator Plugin APIs | Browser APIs |
| Can | Read/modify layers, shapes, keyframes; control timeline; access selection; store plugin data | Render UI with React/Vue/HTML; make network requests; handle user input |
| Cannot | Update the DOM; access browser APIs; make network requests | Access plugin APIs; access the animation scene |
Communication: UI to Plugin
UI sends messages using parent.postMessage(). Messages must be wrapped in a pluginMessage object:
// In plugin UI (src/app.tsx)
function App() {
const createRectangle = () => {
parent.postMessage(
{ pluginMessage: { type: 'create-rectangle', color: '#00ff00' } },
'*'
);
};
return <button onClick={createRectangle}>Create Rectangle</button>;
}Communication: Plugin Receives and Responds
The plugin sandbox listens with creator.ui.onMessage(). Messages arrive unwrapped (just the inner object):
// In plugin sandbox (plugin/plugin.ts)
creator.ui.onMessage((message) => {
if (message.type === 'create-rectangle') {
const layer = creator.activeScene.createShapeLayer({
position: { x: 100, y: 100 }
});
layer.createRectangle({ size: { width: 200, height: 200 } });
layer.createFill({
type: 'SOLID',
color: { r: 0, g: 255, b: 0 }
});
// Send response back to UI (no wrapping needed)
creator.ui.postMessage({ type: 'success' });
}
});Communication: UI Receives Response
The UI listens with window.addEventListener('message', ...). Messages from the plugin arrive wrapped in pluginMessage:
// In plugin UI (src/app.tsx)
useEffect(() => {
const handler = (event: MessageEvent) => {
const message = event.data.pluginMessage;
if (message?.type === 'success') {
console.log('Rectangle created!');
}
};
window.addEventListener('message', handler);
return () => window.removeEventListener('message', handler);
}, []);Request/Response Pattern with messageId
For correlating responses to specific requests, include a messageId:
// UI sends with messageId
const messageId = crypto.randomUUID();
parent.postMessage({
pluginMessage: { type: 'import-svg', content: svgString, messageId }
}, '*');
// Plugin responds with same messageId
creator.ui.onMessage(async (msg) => {
if (msg.type === 'import-svg') {
try {
const layer = await creator.activeScene.import({
type: 'SVG',
content: msg.content,
});
creator.ui.postMessage({
type: 'import-success',
data: { layerId: layer.id },
messageId: msg.messageId,
});
} catch (error) {
creator.ui.postMessage({
type: 'import-error',
data: { error: error instanceof Error ? error.message : 'Unknown error' },
messageId: msg.messageId,
});
}
}
});Type-Safe Message Patterns
Define shared message types to prevent mismatches:
// shared/types.ts
export type PluginMessage =
| { type: 'create-shape'; color: string }
| { type: 'import-svg'; content: string }
| { type: 'delete-selection' };
// In plugin UI
import type { PluginMessage } from '../shared/types';
function handleCreateShape() {
const message: PluginMessage = { type: 'create-shape', color: '#ff0000' };
parent.postMessage({ pluginMessage: message }, '*');
}
// In plugin sandbox
import type { PluginMessage } from '../shared/types';
creator.ui.onMessage((msg: PluginMessage) => {
if (msg.type === 'create-shape') {
console.log(msg.color); // TypeScript knows this has .color
}
});UI API Reference
creator.ui.show(opts?)
Display the plugin UI window:
creator.ui.show({ width: 300, height: 400 });creator.ui.resize(opts)
Resize the plugin UI window (must specify at least width or height):
creator.ui.resize({ width: 400, height: 600 });creator.ui.postMessage(message)
Send data from plugin to UI:
creator.ui.postMessage({
type: 'selection-changed',
count: creator.selection.nodes.length
});creator.ui.onMessage(callback)
Receive messages from UI in plugin code:
creator.ui.onMessage((msg) => {
if (msg.type === 'create-rectangle') {
// Handle message
}
});UI Styling
The UI runs in a standard iframe. Any CSS approach works:
- Inline styles
- CSS files
- Tailwind CSS
- CSS modules
- Styled-components / CSS-in-JS
Importing Assets
Plugins can import external assets into the Creator scene using scene.import().
Supported Formats
| Type | Formats | Returns |
|---|---|---|
'LOTTIE' | Lottie JSON (.json), dotLottie (.lottie) | SceneLayer |
'SVG' | SVG (.svg) | SceneLayer |
'IMAGE' | PNG, JPEG, WebP | ImageLayer |
Importing from URL
// Lottie animation
const animation = await creator.activeScene.import({
type: 'LOTTIE',
url: 'https://example.com/animation.json'
});
// Image
const image = await creator.activeScene.import({
type: 'IMAGE',
url: 'https://example.com/image.png'
});
// SVG
const svg = await creator.activeScene.import({
type: 'SVG',
url: 'https://example.com/graphic.svg'
});Importing from Content String
// Lottie JSON string
const animation = await creator.activeScene.import({
type: 'LOTTIE',
content: lottieJsonString
});
// SVG markup string
const svg = await creator.activeScene.import({
type: 'SVG',
content: '<svg width="100" height="100">...</svg>'
});Note: IMAGE type does not support content — only url.
Working with Imported Layers
Imported layers behave like any other layer. Position, scale, and animate them:
const animation = await scene.import({
type: 'LOTTIE',
url: 'https://example.com/animation.json'
});
animation.name = 'My Animation';
animation.position.staticValue = { x: 100, y: 100 };
animation.scale.staticValue = { x: 50, y: 50 }; // 50% scaleCentering and Scaling Pattern
Common pattern for importing and centering content in the scene:
const scene = creator.activeScene;
const sceneSize = scene.size;
const sceneCenter = { x: sceneSize.width / 2, y: sceneSize.height / 2 };
const layer = await scene.import({ type: 'SVG', content: svgString });
layer.name = 'Imported SVG';
// Get the imported content's dimensions
const importedSize = layer.type === 'SCENE_LAYER'
? layer.scene.size
: { width: layer.image.width, height: layer.image.height };
// Scale to fit within target size
const targetSize = { width: 200, height: 200 };
const scale = Math.min(
targetSize.width / importedSize.width,
targetSize.height / importedSize.height
);
layer.scale.staticValue = { x: scale * 100, y: scale * 100 };
// Center in scene
const scaledWidth = importedSize.width * scale;
const scaledHeight = importedSize.height * scale;
layer.position.staticValue = {
x: sceneCenter.x,
y: sceneCenter.y,
};Network Requests and External Libraries
Network Requests
Plugin sandbox code cannot make network requests directly. To fetch data from external APIs, make the request from UI code and send the result to the plugin sandbox.
Step 1: UI Fetches Data
// In plugin UI (src/app.tsx)
const response = await fetch(`https://api.iconlibrary.com/icons/${iconId}.svg`);
const svgContent = await response.text();Step 2: UI Sends Data to Plugin
// In plugin UI (src/app.tsx)
parent.postMessage({
pluginMessage: {
type: 'import-svg',
content: svgContent
}
}, '*');Step 3: Plugin Receives and Processes
// In plugin sandbox (plugin/plugin.ts)
creator.ui.onMessage(async (msg) => {
if (msg.type === 'import-svg') {
const svgLayer = await creator.activeScene.import({
type: 'SVG',
content: msg.content
});
}
});Step 4: UI Listens for Response
// In plugin UI (src/app.tsx)
window.addEventListener('message', (event) => {
if (
event.data.pluginMessage &&
event.data.pluginMessage.type === 'import-success'
) {
setGenerating(false);
}
});External Libraries
Using Libraries with Bundlers (React Template)
The React template uses Vite for bundling. Install npm packages normally:
pnpm install react-colorfulThen import and use in UI components:
import { HexColorPicker } from 'react-colorful';
function ColorSelector() {
const [color, setColor] = useState('#000000');
const handleChange = (newColor: string) => {
setColor(newColor);
parent.postMessage({
pluginMessage: { type: 'set-color', color: newColor }
}, '*');
};
return <HexColorPicker color={color} onChange={handleChange} />;
}Using Libraries with Plain HTML/JS
Include libraries via CDN script tags:
<script src="https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/pickr.min.js"></script>Key Constraint
External libraries can only be used in UI code (src/). The plugin sandbox (plugin/) runs in isolation and cannot import external packages at runtime.
Scene Graph and Nodes
Scene Hierarchy
Creator organizes animation content in a hierarchy:
File
├── Scene (main)
│ ├── ShapeLayer
│ │ ├── Group
│ │ │ ├── Rectangle
│ │ │ └── Ellipse
│ │ └── Star
│ ├── ImageLayer
│ ├── TextLayer
│ └── SceneLayer ──references──► Nestable Scene
└── Nestable Scene
└── ShapeLayer
└── ...Scenes
Every Creator file has one or more scenes. A scene defines the canvas, framerate, duration, and contains layers.
const scene = creator.activeScene;
// Scene properties
scene.size; // { width: number, height: number }
scene.backgroundColor; // { r, g, b } (preview only, not exported)
scene.framerate; // number (FPS)
scene.duration; // number (seconds)
scene.isNestableScene; // boolean
scene.layers; // ReadonlyArray<Layer>
// Modify scene
scene.size = { width: 1920, height: 1080 };
scene.framerate = 60;
scene.duration = 5;
scene.backgroundColor = { r: 255, g: 0, b: 0 };
// Access all scenes
const allScenes = creator.scenes;
// Create a new scene
const newScene = creator.createScene({
name: 'New Scene',
size: { width: 1920, height: 1080 },
framerate: 60,
duration: 5
});
// Switch active scene
creator.switchToScene(newScene);Nested Scenes and Scene Layers
A scene can be nestable (child of another scene via a scene layer):
- Scene layer — A layer that references a nestable scene. It behaves like a regular layer but its content comes from the referenced scene.
- Changes to the source scene automatically reflect in all instances.
// Access the referenced scene
const sourceScene = sceneLayer.scene;
// Break the connection (converts to regular layer)
sceneLayer.break();Layer Types
Layers are top-level elements of a scene:
| Type Constant | Interface | Description |
|---|---|---|
'SHAPE_LAYER' | ShapeLayer | Contains shapes, fills, strokes, trim paths |
'IMAGE_LAYER' | ImageLayer | Contains an image asset |
'TEXT_LAYER' | TextLayer | Contains text content |
'SCENE_LAYER' | SceneLayer | References another scene |
All layers share common properties from LayerMixin:
// Common layer properties
layer.id; // readonly string
layer.name; // string (read/write)
layer.type; // layer type constant
layer.visible; // boolean
layer.locked; // boolean
layer.focused; // boolean
layer.startFrame; // number
layer.endFrame; // number
layer.timelineOffset; // number
layer.blendMode; // BlendMode
// Common animatable properties (TransformMixin)
layer.position; // Animatable<Vector>
layer.rotation; // Animatable<number>
layer.scale; // Animatable<Vector>
layer.opacity; // Animatable<number> (0-100)
layer.skew; // Animatable<number>
layer.skewAxis; // Animatable<number>
// Common methods
layer.clone(); // Duplicate the layer
layer.remove(); // Remove from scene
layer.align('left'); // Align within parent
layer.flip('horizontal'); // Flip direction
// Masks
layer.masks; // ReadonlyArray<Mask>
layer.createMask({ mode: 'add', pathData, opacity: 100 });
// Mattes
layer.isMatte; // boolean
layer.matte; // Matte | undefinedType-Specific Properties
// ShapeLayer — has shapes and styling
if (layer.type === 'SHAPE_LAYER') {
layer.shapes; // ReadonlyArray<Shape>
layer.fills; // ReadonlyArray<Paint>
layer.strokes; // ReadonlyArray<Stroke>
layer.trimPaths; // ReadonlyArray<TrimPath>
}
// ImageLayer — has image asset
if (layer.type === 'IMAGE_LAYER') {
layer.image; // Image { type, width, height }
}
// TextLayer — has text content
if (layer.type === 'TEXT_LAYER') {
layer.text; // string (read/write)
}
// SceneLayer — references another scene
if (layer.type === 'SCENE_LAYER') {
layer.scene; // Scene
layer.break(); // Break connection to source scene
}Shape Types
Shapes are children of ShapeLayer or Group:
| Type Constant | Interface | Specific Properties |
|---|---|---|
'RECTANGLE' | Rectangle | size, position, roundness |
'ELLIPSE' | Ellipse | size, position |
'POLYGON' | Polygon | points, position, rotation, outerRadius, outerRoundness |
'STAR' | Star | points, position, rotation, innerRadius, outerRadius, innerRoundness, outerRoundness |
'PATH' | Path | pathData |
'GROUP' | Group | Contains other shapes; has shapes, fills, strokes, trimPaths, opacity, blendMode |
Accessing Scene Content
Via Scene
const scene = creator.activeScene;
const layers = scene.layers;
for (const layer of layers) {
console.log(layer.name, layer.type);
}Via Selection
const selectedNodes = creator.selection.nodes; // Layers and shapes
const selectedKeyframes = creator.selection.keyframes;Common Traversal Patterns
Filter by Type
// Get only image layers from selection
const imageLayers = creator.selection.nodes.filter(
(node): node is ImageLayer => node.type === 'IMAGE_LAYER'
);Traverse Down (Recursive Shape Visitor)
function visitShapes(parent: ShapeLayer | Group, callback: (shape: Shape) => void) {
for (const shape of parent.shapes) {
callback(shape);
if (shape.type === 'GROUP') {
visitShapes(shape, callback);
}
}
}
// Find all shapes in selected layers
const selection = creator.selection.nodes;
const shapes: Shape[] = [];
selection.forEach(node => {
if (node.type === 'SHAPE_LAYER') {
visitShapes(node, (shape) => shapes.push(shape));
}
});Layer Type Guard
function isLayer(node: Layer | Shape): node is Layer {
return (
node.type === 'SHAPE_LAYER' ||
node.type === 'SCENE_LAYER' ||
node.type === 'IMAGE_LAYER' ||
node.type === 'TEXT_LAYER'
);
}Shapes, Styling, and Animation
Creating Shapes
First create a shape layer, then add shapes to it:
const layer = creator.activeScene.createShapeLayer();
const rect = layer.createRectangle({ size: { width: 200, height: 150 } });Shapes need a fill or stroke to be visible.
Shape Creation Methods
All available on ShapeLayer and Group:
// Rectangle
layer.createRectangle({ position?: Vector, size?: Size, roundness?: number });
// Ellipse
layer.createEllipse({ position?: Vector, size?: Size });
// Polygon
layer.createPolygon({
position?: Vector, rotation?: number,
points?: number, outerRadius?: number, outerRoundness?: number
});
// Star
layer.createStar({
position?: Vector, rotation?: number,
points?: number, innerRadius?: number, outerRadius?: number,
innerRoundness?: number, outerRoundness?: number
});
// Path (custom bezier)
layer.createPath({
points?: PathPoint[], closed?: boolean
});
// Group (combine shapes)
layer.createGroup({ shapes: [rect, ellipse] });PathPoint Format
const point: PathPoint = {
vertex: { x: 0, y: 0 }, // Point position
inTan: { x: 0, y: 0 }, // Incoming tangent
outTan: { x: 0, y: 0 }, // Outgoing tangent
};Styling
Fills
Fills and strokes are created on ShapeLayer or Group and apply to all shapes within.
// Solid fill
layer.createFill({
type: 'SOLID',
color: { r: 66, g: 133, b: 244 }
});
// Linear gradient
layer.createFill({
type: 'GRADIENT_LINEAR',
start: { x: 0, y: 100 }, // Optional (defaults to left edge)
end: { x: 200, y: 100 }, // Optional (defaults to right edge)
stops: [
{ color: { r: 255, g: 0, b: 0 }, offset: 0, opacity: 1 },
{ color: { r: 0, g: 0, b: 255 }, offset: 1, opacity: 1 }
]
});
// Radial gradient
layer.createFill({
type: 'GRADIENT_RADIAL',
start: { x: 100, y: 100 }, // Optional (defaults to center)
end: { x: 200, y: 100 }, // Optional (defaults to right edge)
highlightAngle: 45, // Optional
highlightLength: 50, // Optional (-100 to 100)
stops: [
{ color: { r: 255, g: 255, b: 255 }, offset: 0, opacity: 1 },
{ color: { r: 0, g: 0, b: 0 }, offset: 1, opacity: 1 }
]
});Strokes
// Solid stroke
layer.createStroke({
fill: { type: 'SOLID', color: { r: 0, g: 0, b: 0 } },
width: 2
});
// Gradient stroke
layer.createStroke({
fill: {
type: 'GRADIENT_LINEAR',
stops: [
{ color: { r: 255, g: 0, b: 0 }, offset: 0, opacity: 1 },
{ color: { r: 0, g: 0, b: 255 }, offset: 1, opacity: 1 }
]
},
width: 5
});Modifying Existing Styles
// Change fill color
layer.fills[0].color.staticValue = { r: 255, g: 0, b: 0 };
// Remove a fill/stroke
layer.fills[0].remove();
layer.strokes[0].remove();Trim Paths
Trim paths control how much of a stroke path is drawn:
const trimPath = layer.createTrimPath({
start: 0, // Percentage (0-100)
end: 100, // Percentage (0-100)
offset: 0 // Degrees (0-360)
});
// Animate trim path for draw-on effect
trimPath.end.addKeyframes([
{ frame: 0, value: 0 },
{ frame: 60, value: 100 }
]);Animation
Animatable Properties
Any property implementing Animatable<T> can be animated:
interface Animatable<T> {
staticValue: T; // Value when not animated
readonly isAnimated: boolean; // Has keyframes?
readonly keyframes: ReadonlyArray<Keyframe<T>>; // All keyframes
getKeyframeAt(frame: number): Keyframe<T> | undefined;
addKeyframes(keyframes: Array<KeyframeAdd<T>>): void;
clearKeyframes(): void;
}Transform Properties (Common to Layers and Groups)
| Property | Type | Notes |
|---|---|---|
position | Animatable<Vector> | X/Y position |
rotation | Animatable<number> | Degrees |
scale | Animatable<Vector> | Percentage (100 = 100%) |
opacity | Animatable<number> | 0 to 100 |
skew | Animatable<number> | Degrees |
skewAxis | Animatable<number> | Degrees |
Shape-Specific Animatable Properties
| Shape | Properties |
|---|---|
| Rectangle | size: Animatable<Size>, roundness: Animatable<number> |
| Ellipse | size: Animatable<Size> |
| Polygon | points, outerRadius, outerRoundness |
| Star | points, innerRadius, outerRadius, innerRoundness, outerRoundness |
| Path | pathData: Animatable<PathData> |
Style Animatable Properties
| Style | Properties |
|---|---|
| Solid fill | color: Animatable<Color> |
| Gradient fill | start, end: Animatable<Vector>, stops: Animatable<ColorStops> |
| Radial gradient | Above + highlightAngle, highlightLength: Animatable<number> |
| Stroke | width: Animatable<number> |
| Trim path | start, end: Animatable<number>, offset: Animatable<number> |
Adding Keyframes
// Simple animation
layer.position.addKeyframes([
{ frame: 0, value: { x: 100, y: 100 } },
{ frame: 60, value: { x: 400, y: 100 } }
]);
// With easing
layer.position.addKeyframes([
{ frame: 0, value: { x: 50, y: 100 }, easing: { type: 'CUBIC_BEZIER', x1: 0.42, y1: 0, x2: 0.58, y2: 1 } },
{ frame: 60, value: { x: 350, y: 100 } }
]);Easing Types
// Linear (constant speed)
{ type: 'LINEAR' }
// Cubic bezier (customizable curve)
{ type: 'CUBIC_BEZIER', x1: number, y1: number, x2: number, y2: number }Common cubic bezier presets:
- Ease in:
x1: 0.42, y1: 0, x2: 1, y2: 1 - Ease out:
x1: 0, y1: 0, x2: 0.58, y2: 1 - Ease in-out:
x1: 0.42, y1: 0, x2: 0.58, y2: 1
Updating Existing Keyframes
// Check if animated
const isAnimated = layer.position.isAnimated;
// Read keyframes
const keyframes = layer.position.keyframes;
// Update a keyframe value
const kf = layer.position.getKeyframeAt(30);
if (kf) {
kf.value = { x: kf.value.x + 20, y: kf.value.y + 20 };
kf.easing = { type: 'CUBIC_BEZIER', x1: 0.42, y1: 0, x2: 0.58, y2: 1 };
}
// Remove a specific keyframe
kf.remove();
// Remove all keyframes
layer.position.clearKeyframes();Grouping and Animating Together
const group = layer.createGroup({ shapes: [rect, ellipse] });
const endFrame = scene.duration * scene.framerate;
// Rotate the entire group
group.rotation.addKeyframes([
{ frame: 0, value: 0 },
{ frame: endFrame, value: 360 }
]);Storage, Events, and Timeline
Storage Options
| Storage | Persisted Where | Saved with File | Limit | Value Types |
|---|---|---|---|---|
creator.clientStorage | User's browser | No | 5 MB per plugin | Boolean, number, string, object, array |
node.data | Animation file | Yes (not exported) | 5 KB per node | Strings only |
Client Storage
Persists across sessions but is local to the user's browser. Ideal for user preferences, plugin settings, and cached data.
// Save data
await creator.clientStorage.set('lastUsedColor', '#FF5733');
await creator.clientStorage.set('preferences', { theme: 'dark', grid: true });
// Retrieve data
const color = await creator.clientStorage.get('lastUsedColor');
const prefs = await creator.clientStorage.get('preferences');
// List all keys
const keys = await creator.clientStorage.keys();
// Check usage
const usedBytes = await creator.clientStorage.usedQuota();
// Delete specific key
await creator.clientStorage.delete('lastUsedColor');
// Clear all plugin data
await creator.clientStorage.clear();Important notes:
- Data is specific to the plugin ID. Changing the ID loses access.
- Data can be inspected via browser DevTools. Avoid storing sensitive data.
- Data may be cleared if the user clears browser data.
- Accessible only from plugin sandbox code (not directly from UI).
Node Data
Stored on individual nodes within the animation file. Ideal for layer metadata and plugin state specific to the current project.
const layer = creator.activeScene.layers[0];
// Store string values (5 KB limit per node)
layer.data.set('customId', 'my-special-layer');
// For complex data, stringify first (values must be strings)
layer.data.set('metadata', JSON.stringify({
created: Date.now(),
author: 'Plugin User'
}));
// Retrieve data
const customId = layer.data.get('customId');
const metadata = JSON.parse(layer.data.get('metadata') ?? '{}');
// List keys
const keys = layer.data.keys; // Note: property, not method
// Check usage
const usedBytes = layer.data.usedQuota; // Note: property, not method
// Delete
layer.data.delete('customId');
// Clear all plugin data from this node
layer.data.clear();Important notes:
- Saved within Creator's animation file but NOT retained when exported to Lottie.
- Data is specific to the plugin ID.
- Values must be strings. Use
JSON.stringify()/JSON.parse()for complex data.
Events
Selection Events
// Listen for node selection changes
creator.on('selection:nodes', (nodes) => {
console.log('Selected nodes:', nodes.length);
creator.ui.postMessage({
type: 'selection-changed',
data: nodes.map(n => ({ id: n.id, name: n.name, type: n.type }))
});
});
// Listen for keyframe selection changes
creator.on('selection:keyframes', (keyframes) => {
console.log('Selected keyframes:', keyframes.length);
});
// Remove listener
const handler = (nodes) => { /* ... */ };
creator.on('selection:nodes', handler);
creator.off('selection:nodes', handler);Selection API
// Read current selection
const selectedNodes = creator.selection.nodes; // ReadonlyArray<Layer | Shape>
const selectedKeyframes = creator.selection.keyframes; // ReadonlyArray<Keyframe<unknown>>Timeline API
// Read state
const currentFrame = creator.timeline.currentFrame;
const isPlaying = creator.timeline.isPlaying;
// Control playback
creator.timeline.play();
creator.timeline.pause();
creator.timeline.goToFrame(60);Other Global APIs
// Open external link
creator.openLink('https://lottiefiles.com');
// Close the plugin
creator.closePlugin();
// User info (requires "user" permission in manifest)
const userId = creator.user?.id;
const userName = creator.user?.name;
const userToken = creator.user?.token;
// Workspace info (requires "workspaces" permission in manifest)
const currentWorkspace = creator.currentWorkspace; // { id, name }
const allWorkspaces = creator.workspaces; // Array<{ id, name }>