Command Palette

Search for a command to run...

Guide to Modifying Lottie Files

Learn how to modify Lottie files programmatically using relottie. This guide covers transformer plugins, tree traversal, and common modification patterns for Lottie animations.

Guide: Modifying Lottie Files

One of the primary reasons to use relottie is to programmatically modify Lottie JSON files. This is achieved by creating or using transformer plugins that manipulate the LAST (Lottie Abstract Syntax Tree).

The Core Mechanism: Transformer Plugins

Recall the Relottie Core Concepts: the processor parses the Lottie JSON into a LAST tree, runs transformer plugins on that tree, and then stringifies the tree back to JSON.

Modifications happen within these transformer plugins. A transformer plugin is essentially a function that receives the LAST tree (and the VFile) and can make changes to it directly.

import type { Plugin, Transformer } from "unified";
import type { Root, Parent } from "@lottiefiles/last";
import type { VFile } from "vfile";

interface MyPluginOptions {
  someCondition?: boolean;
}

const myModificationPlugin: Plugin<[MyPluginOptions?], Root> = (options = {}) => {
  const transformerFn: Transformer<Root, Root> = (tree: Root, file: VFile) => {
    if (options.someCondition) {
      // Example: conditionally do something
    }
  };
  return transformerFn;
};

Traversing the Tree

To modify specific parts of the Lottie, you first need to find the relevant nodes within the LAST tree. The most common way to do this is using tree traversal utilities from the unist ecosystem, like unist-util-visit.

visit allows you to specify which node types (or specific nodes) you want to inspect and provides a visitor function that gets called for each matching node.

import { visit } from "unist-util-visit";
import type { Plugin } from "unified";
import type { Root, Attribute } from "@lottiefiles/last";

const myVisitorPlugin: Plugin<[], Root> = () => {
  return (tree: Root) => {
    visit(tree, "attribute", (node: Attribute) => {
      console.log(`Found attribute with key: ${node.key}`);
    });

    visit(tree, "primitive", (node) => {
      if (typeof node.value === "number" && node.value > 100) {
        console.log("Found a large primitive number:", node.value);
      }
    });
  };
};

Common Modification Patterns

Based on the examples in the source documentation:

1. Changing Primitive Values

This is often the simplest modification. Find the target node (e.g., an Attribute like the Lottie version) and update the value property of its child Primitive node.

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

interface ChangeFramerateOptions {
  newFramerate?: number;
}

const changeFrameratePlugin: Plugin<[ChangeFramerateOptions?], Root> = (options = {}) => {
  const { newFramerate = 30 } = options;

  const transformer: Transformer<Root, Root> = (tree) => {
    visit(tree, "attribute", (node: Attribute) => {
      if (node.title === TITLES.number.framerate) {
        const primitiveChild = node.children[0] as Primitive | undefined;

        if (primitiveChild && primitiveChild.type === "primitive") {
          console.log(`Old framerate: ${primitiveChild.value}`);
          primitiveChild.value = newFramerate;
          primitiveChild.valueType = "number";
          console.log(`New framerate set to: ${primitiveChild.value}`);
          return EXIT;
        }
      }
      return CONTINUE;
    });
  };
  return transformer;
};

(See also the version update example in Quick Start and Usage Guide).

2. Changing Node Properties

You can modify properties of the nodes themselves, such as changing the key or title of an Attribute, Element, or Collection. However, changing title should be done with extreme caution as it alters the semantic meaning of the node and could lead to an invalid or misinterpreted Lottie structure.

import { visit, CONTINUE } from "unist-util-visit";
import type { Plugin, Transformer } from "unified";
import type { Root, Attribute } from "@lottiefiles/last";
import { TITLES } from "@lottiefiles/last";

const renameGeneratorPlugin: Plugin<[], Root> = () => {
  const transformer: Transformer<Root, Root> = (tree) => {
    visit(tree, "attribute", (node: Attribute) => {
      if (node.title === TITLES.string.generator) {
        console.log(
          `Original generator key: '${node.key}', title: '${node.title}', value: '${node.children[0]?.value}'`
        );
        if (node.children[0]?.type === "primitive") {
          (node.children[0] as Primitive).value = "My Custom Generator";
        }
        return CONTINUE;
      }
    });
  };
  return transformer;
};

3. Adding Nodes

To add new elements, such as a custom metadata field, you create new LAST nodes (ideally using @lottiefiles/last-builder) and insert them into the children array of an appropriate parent node (e.g., an ObjectNode for new metadata attributes, or the Root node for new top-level elements like markers).

Example: Adding a Custom Metadata Attribute

import { visit, EXIT, CONTINUE } from "unist-util-visit";
import type { Plugin, Transformer } from "unified";
import type { Root, Element, ObjectNode, Attribute } from "@lottiefiles/last";
import { TITLES } from "@lottiefiles/last";
import { at, pt } from "@lottiefiles/last-builder";

interface AddCustomMetadataOptions {
  metadataKey: string;
  metadataValue: string;
}

const addCustomMetadataPlugin: Plugin<[AddCustomMetadataOptions], Root> = (options) => {
  const { metadataKey, metadataValue } = options;

  if (!metadataKey || !metadataValue) {
    console.warn("Custom metadata key or value not provided. Skipping plugin.");
    return (tree: Root) => tree;
  }

  const transformer: Transformer<Root, Root> = (tree) => {
    let metaElementModified = false;
    visit(tree, "element", (node: Element, index: number | undefined, parent: Parent | undefined) => {
      if (node.title === TITLES.element.metadata) {
        const objectNode = node.children[0] as ObjectNode | undefined;
        if (objectNode && objectNode.type === "object") {
          const existingAttr = objectNode.children.find(
            (child) => child.type === "attribute" && child.key === metadataKey
          );
          if (!existingAttr) {
            const newAttribute = at(metadataKey, TITLES.custom, pt(metadataValue));
            objectNode.children.push(newAttribute as Attribute);
            console.log(`Added custom metadata: ${metadataKey} = ${metadataValue}`);
          } else {
            console.log(`Custom metadata '${metadataKey}' already exists.`);
          }
          metaElementModified = true;
          return EXIT;
        }
      }
      return CONTINUE;
    });

    if (!metaElementModified) {
      console.log("No 'meta' element found to add custom metadata to.");
    }
  };
  return transformer;
};

Risks when adding nodes:

  • Invalid Structure: Adding nodes in the wrong place or with incorrect types can break the Lottie file.

  • Player Compatibility: Custom or non-standard nodes/attributes might be ignored by Lottie players or cause unexpected behavior.

  • Increased File Size: Adding many nodes can significantly increase file size.

4. Removing Nodes

To remove nodes, you find the target node and then remove it from its parent's children array. You need the parent node and the index of the child to do this safely. unist-util-visit provides both.

Example: Removing All Markers

import { visit, CONTINUE, EXIT } from "unist-util-visit";
import type { Plugin, Transformer } from "unified";
import type { Root, Collection, Parent } from "@lottiefiles/last";
import { TITLES } from "@lottiefiles/last";

const removeMarkersPlugin: Plugin<[], Root> = () => {
  const transformer: Transformer<Root, Root> = (tree) => {
    visit(tree, "collection", (node: Collection, index: number | undefined, parent: Parent | undefined) => {
      if (node.title === TITLES.collection.markers && parent && parent.type === "root" && typeof index === "number") {
        (parent as Root).children.splice(index, 1);
        console.log('Removed the entire "markers" collection.');
        return EXIT;
      }
      return CONTINUE;
    });
  };
  return transformer;
};

Risks when removing nodes:

  • Broken Dependencies: Removing a node that other parts of the animation depend on (e.g., an asset in assets that a layer references) will break the animation.

  • Functionality Loss: Removing elements like layers, shapes, or effects will alter the animation's appearance or behavior.

  • Invalid Lottie: Removing essential structural nodes might render the Lottie file invalid.

5. Replacing Nodes

Replacing nodes involves removing an old node and inserting a new one at the same position. This is effectively a combination of removal and addition.

import { visit, CONTINUE, EXIT } from "unist-util-visit";
import type { Plugin, Transformer } from "unified";
import type { Root, Attribute, Primitive, Parent } from "@lottiefiles/last";
import { TITLES } from "@lottiefiles/last";
import { pt } from "@lottiefiles/last-builder";

const replaceAnimationNamePlugin: Plugin<[{ newName: string }], Root> = (options) => {
  const { newName } = options;
  if (!newName) return (tree: Root) => tree;

  const transformer: Transformer<Root, Root> = (tree) => {
    visit(tree, "attribute", (node: Attribute, index: number | undefined, parent: Parent | undefined) => {
      if (node.title === TITLES.string.name && parent && parent.type === "root" && typeof index === "number") {
        const oldName = (node.children[0] as Primitive)?.value;
        const newNamePrimitive = pt(newName);
        node.children = [newNamePrimitive as Primitive];
        console.log(`Replaced animation name from '${oldName}' to '${newName}'.`);
        return EXIT;
      }
      return CONTINUE;
    });
  };
  return transformer;
};

Risks when replacing nodes: Similar to adding and removing, ensure the new node is valid for its position and doesn't break dependencies.

Important Considerations

  • Understand LAST: Effective modification requires a good understanding of the LAST Specification to know which nodes and properties to target.

  • Immutability (Best Practice): While direct modification is possible, for more complex scenarios or safer updates, consider patterns that treat the tree as immutable (e.g., creating copies of nodes before modifying them), although unist-util-visit modifies in place by default.

  • Validation: relottie itself doesn't inherently validate that your modifications result in a semantically correct Lottie file (e.g., changing a layer type without updating its properties might lead to unexpected rendering). Validation would typically be a separate step or require a dedicated validation plugin.

Next Steps

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