Command Palette

Search for a command to run...

Guide to Creating relottie Plugins

Learn how to create custom relottie plugins to transform and analyze Lottie animations. This guide covers plugin fundamentals, structure, typing, and provides a practical example.

Guide: Creating relottie Plugins

relottie's power comes from its plugin ecosystem. While you can use existing plugins for common tasks like analysis and metadata extraction, you'll often want to create custom plugins to implement specific transformations or logic tailored to your needs.

Plugin Fundamentals

A relottie plugin is essentially a function that integrates with the unified processing pipeline. Specifically, most custom plugins you write will be transformer plugins. These plugins receive the LAST (Lottie Abstract Syntax Tree) and can modify it, or they can inspect it and attach data to the VFile being processed.

Basic Structure

A transformer plugin typically follows this structure:

import type { Plugin, Transformer } from "unified";
import type { Root } from "@lottiefiles/last";
import type { VFile, Data } from "vfile"; // For interacting with the virtual file

// 1. Define an interface for your plugin's options (optional)
export interface MyPluginOptions {
  someSetting?: boolean;
  valueToUse?: string;
}

// 2. Define an interface for any data your plugin might attach to VFile.data (optional)
export interface MyPluginFileData extends Data {
  myPluginResult?: {
    info: string;
    count: number;
  };
}

// 3. The plugin function itself
// It typically accepts options and returns the core transformer function.
const myCustomPlugin: Plugin<[MyPluginOptions?], Root> = (
  options: MyPluginOptions = {} // Provide default options if needed
) => {
  // Default values for options can be set here
  const { someSetting = false, valueToUse = "default" } = options;

  // 4. The transformer function: this does the actual work on the tree and file
  const transformer: Transformer<Root, Root> = (tree: Root, file: VFile) => {
    // Logic to inspect and/or modify the 'tree' (LAST) goes here.
    // You have access to 'options' (e.g., 'someSetting', 'valueToUse').

    // Example: Attaching data to the VFile
    const pluginDataStore: MyPluginFileData = (file.data as MyPluginFileData) || {};
    pluginDataStore.myPluginResult = {
      info: `Processed with value: ${valueToUse}`,
      count: 0,
    };
    // Make sure to assign it back if file.data was initially undefined
    file.data = pluginDataStore;

    // If modifying the tree, you'd directly manipulate 'tree' object.
    // e.g., using unist-util-visit to find and change nodes.
  };

  return transformer;
};

export default myCustomPlugin;
  • Outer Function (myCustomPlugin):

    • Takes optional configuration (options). It's good practice to define an interface for these options (MyPluginOptions).

    • Returns the actual transformer function.

  • Plugin<[MyPluginOptions?], Root> Signature:

    • This is a generic type from unified.

    • The first type parameter ([MyPluginOptions?]) is an array defining the expected types for the options passed to .use(plugin, ...options). ? makes the options themselves optional for the .use call.

    • The second type parameter (Root) specifies the type of the AST node the plugin primarily operates on (usually Root for the entire Lottie tree).

  • Inner Transformer Function (transformer):

    • This is where the core logic resides. It receives:

      • tree: The Root node of the LAST.

      • file: The VFile object associated with the current processing pipeline. This is crucial for:

        • Accessing File Path: file.path (if the input was a file or path was provided).

        • Reporting Messages: file.message('Something happened', node) can be used to report warnings or errors, optionally associated with a specific AST node (for positional information).

        • Sharing Data: file.data is a key-value store where plugins can attach information. For example, relottie-metadata attaches its findings to file.data.metadata. Your plugin can read data set by previous plugins or write its own. Define an interface (e.g., MyPluginFileData) for type safety if your plugin uses file.data.

    • The transformer can directly modify the tree object. These modifications are then passed to the next plugin or to the stringifier.

Typing Your Plugins (TypeScript)

Using TypeScript and correctly typing your plugins is strongly recommended for clarity, maintainability, and preventing errors:

  • Use Plugin and Transformer from unified: Import these types and use their generics as shown above.

  • Use last Node Types: Import specific node types (Root, Attribute, Element, Primitive, Parent etc.) from @lottiefiles/last for type safety when working with nodes within your transformer (e.g., when using unist-util-visit).

  • Define Option and VFile.data Interfaces: Clearly type any options your plugin accepts and any data it adds to VFile.data.

Example: Text Layer Modifier and Reporter Plugin

Let's create a new plugin that performs two actions:

  1. Modifies the text content of all text layers by appending a suffix.

  2. Collects the original and new text values and reports them via VFile.data.

This example uses unist-util-visit for tree traversal.

modify-text-layers-plugin.ts:

import type { Plugin, Transformer } from "unified";
import type { Root, Element, ObjectNode, Attribute, Primitive, Parent } from "@lottiefiles/last";
import { TITLES } from "@lottiefiles/last";
import { visit, CONTINUE, EXIT } from "unist-util-visit";
import type { VFile, Data } from "vfile";

// 1. Plugin Options Interface
export interface ModifyTextOptions {
  suffix?: string;
  ignoreCase?: boolean; // Example of another option
}

// 2. VFile Data Interface for this plugin's output
export interface TextLayersData extends Data {
  textModifications?: {
    original: string;
    modified: string;
    layerName: string;
  }[];
}

// 3. The Plugin
const modifyTextLayersPlugin: Plugin<[ModifyTextOptions?], Root> = (options = {}) => {
  const { suffix = " - Modified!" } = options; // Default suffix

  // 4. The Transformer Function
  const transformer: Transformer<Root, Root> = (tree: Root, file: VFile) => {
    const modifications: TextLayersData["textModifications"] = [];
    let textDocumentNode: ObjectNode | undefined; // To store 't.d' object node

    // Traverse to find text layers and their text content
    visit(tree, "element", (layerNode: Element) => {
      // Check if it's a text layer (ty: 5)
      // A more robust check would be to find the 'ty' attribute within the layerNode first.
      // This simplified check assumes a common structure.
      let isTextLayer = false;
      let layerName = "Unknown Layer";

      if (layerNode.title === TITLES.object.layerText) {
        isTextLayer = true;
      }

      // Attempt to get layer name (nm attribute)
      visit(layerNode, "attribute", (attrNode: Attribute) => {
        if (attrNode.title === TITLES.string.name) {
          layerName = ((attrNode.children[0] as Primitive)?.value as string) || layerName;
          return EXIT; // Found name, stop search in this layerNode for name
        }
        return CONTINUE;
      });

      if (isTextLayer) {
        // Find the Text Document Data: text -> documentData (t.d)
        visit(layerNode, "element", (textElementNode: Element) => {
          if (textElementNode.title === TITLES.element.textData) {
            // Found 't' (Text Properties) element
            visit(textElementNode, "element", (docDataElementNode: Element) => {
              if (docDataElementNode.title === TITLES.element.textDocumentData) {
                // Found 'd' (Document Data) element which contains 'k' (keyframes)
                textDocumentNode = docDataElementNode.children[0] as ObjectNode;
                return EXIT; // Found t.d, stop this inner visit
              }
              return CONTINUE;
            });
            return EXIT; // Processed textData element
          }
          return CONTINUE;
        });

        if (textDocumentNode) {
          // The actual text is often in the first keyframe: textDocumentData -> keyframes -> 0 -> startValue -> text (k[0].s.t)
          // This structure can be complex. For simplicity, we'll look for a direct 't' attribute if it exists,
          // or a common path. A robust plugin would handle various text data structures.
          // This is a simplified traversal looking for 't' (text value) attribute within document data.
          visit(textDocumentNode, "attribute", (textAttr: Attribute) => {
            if (textAttr.title === TITLES.string.text) {
              const textPrimitive = textAttr.children[0] as Primitive | undefined;
              if (textPrimitive && typeof textPrimitive.value === "string") {
                const originalText = textPrimitive.value;
                textPrimitive.value += suffix;
                modifications.push({
                  original: originalText,
                  modified: textPrimitive.value,
                  layerName,
                });
              }
              return EXIT; // Text attribute processed for this layer
            }
            return CONTINUE;
          });
        }
      }
      textDocumentNode = undefined; // Reset for next layer
      return CONTINUE; // Continue to next layer or node
    });

    // Attach collected data to VFile
    if (modifications.length > 0) {
      const pluginVFileData = (file.data as TextLayersData) || {};
      pluginVFileData.textModifications = modifications;
      file.data = pluginVFileData;
      console.log(`Plugin: Modified ${modifications.length} text layer(s).`);
    }
  };

  return transformer;
};

export default modifyTextLayersPlugin;

Using the plugin:

import { relottie } from "@lottiefiles/relottie";
import modifyTextLayersPlugin, { type TextLayersData, type ModifyTextOptions } from "./modify-text-layers-plugin";

const inputLottieWithText =
  '{"v":"5.5.2", "nm":"Text Example", "fr":30, "w":500, "h":100, "layers":[
    {"ddd":0,"ind":1,"ty":5,"nm":"First Text Layer","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,50,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"t":{"d":{"k":[{"s":{"s":24,"f":"Arial","t":"Hello World","j":0,"tr":0,"lh":28.8,"ls":0,"fc":[0,0,0]},"t":0}]},"ip":0,"op":90,"st":0,"bm":0}},
    {"ddd":0,"ind":2,"ty":5,"nm":"Second Text Layer","sr":1,"ks":{},"ao":0,"t":{"d":{"k":[{"s":{"s":18,"f":"Roboto","t":"Another line of text"},"t":0}]},"ip":0,"op":90,"st":0,"bm":0}}
  ]}';

async function runTextModification() {
  try {
    const processor = relottie().use(modifyTextLayersPlugin, { suffix: " (Updated)" } as ModifyTextOptions);
    const file = await processor.process(inputLottieWithText);

    console.log("\\nFinal Lottie JSON:");
  console.log(String(file));

    const fileData = file.data as TextLayersData;
    if (fileData.textModifications && fileData.textModifications.length > 0) {
      console.log("\\nText Modifications Report:");
      fileData.textModifications.forEach(mod => {
        console.log(`- Layer '${mod.layerName}':`);
        console.log(`    Original: "${mod.original}"`);
        console.log(`    Modified: "${mod.modified}"`);
      });
    }
  } catch (error) {
    console.error("Error during text modification:", error);
  }
}

runTextModification();

Expected Output (simplified):

Plugin: Modified 2 text layer(s).

Final Lottie JSON:
{ ... "t":"Hello World (Updated)" ... "t":"Another line of text (Updated)" ...}

Text Modifications Report:
- Layer 'First Text Layer':
    Original: "Hello World"
    Modified: "Hello World (Updated)"
- Layer 'Second Text Layer':
    Original: "Another line of text"
    Modified: "Another line of text (Updated)"

This example covers:

  • Defining plugin options (ModifyTextOptions).

  • Defining a structure for data to be attached to VFile (TextLayersData).

  • Using unist-util-visit to find relevant nodes (text layers, then text data attributes).

  • Modifying the value of Primitive text nodes.

  • Storing results of the modification in file.data.

  • Accessing this data after processing.

Note: Accessing text data in Lottie (specifically t.d.k[0].s.t) can be complex due to keyframes and expressions. This example uses a simplified traversal. A production-ready plugin would need to handle the Lottie text data structure more robustly.

Key Steps in Plugin Development

  1. Define the Goal: What specific transformation or analysis should the plugin perform?

  2. Identify Target Nodes: Determine which last node types and properties you need to interact with by consulting the LAST Specification.

  3. Choose Traversal Strategy: Use unist-util-visit or other unist utilities to find the target nodes.

  4. Implement Logic: Write the code within the transformer function to modify the tree or extract data.

  5. Define Options (Optional): Create an interface for any configuration the plugin might need.

  6. Type Everything: Use TypeScript and the provided types (Plugin, Root, Attribute, etc.) for robustness.

  7. Test: Thoroughly test your plugin with various Lottie file inputs.

Further Exploration

Last updated: April 10, 2026 at 9:12 AMEdit this page