tlottie
v0.1.23
Published
Fast, web-worker based Lottie/TGS renderer with adapters for React, SolidJS, Vue, Svelte, Web Components and Vanilla JS, backed by the tlottie Rust/WASM engine.
Maintainers
Readme
tlottie
Fast, Web Worker–based Lottie and TGS (Telegram sticker) renderer, backed by the tlottie Rust/WASM engine. Ships adapters for React, SolidJS, Vue, Svelte, Web Components, and Vanilla JS.
Live demo → — one page per adapter.
- Off the main thread. Parsing and rendering happen in a configurable pool of Web Workers, drawing into an
OffscreenCanvas— no jank on the UI thread. - Fast core. The underlying tlottie engine benchmarks 23–73% faster frame times than rlottie/thorvg (see its own README for numbers).
- TGS support. Gzipped Lottie (
.tgs, Telegram stickers) is decompressed in-worker using the browser-nativeDecompressionStream— nopako/fflatedependency. - Skeleton loading. Pass an outline SVG and get a CSS
mask-imageshimmer while the animation loads or if it fails — generate that SVG with the bundledtlottie-outlineCLI (bunx tlottie-outline --input animation.json). - Small. Each framework adapter is ~3–4KB gzipped; the shared WASM binary (~418KB raw, ~131KB brotli / ~163KB gzip) is fetched once and cached, not bundled per adapter.
Install
bun add tlottie
# or: npm install tlottie / pnpm add tlottie / yarn add tlottieFramework peer dependencies (react, solid-js, vue, svelte) are optional — only install the one matching the adapter you use.
Quick start
React
import { LottiePlayer } from "tlottie/react";
<LottiePlayer src="/animation.json" loop autoplay />;SolidJS
import { LottiePlayer } from "tlottie/solid";
<LottiePlayer src="/animation.json" loop autoplay />;Vue
<script setup>
import { LottiePlayer } from "tlottie/vue";
</script>
<template>
<LottiePlayer src="/animation.json" loop autoplay />
</template>Svelte
<script>
import { LottiePlayer } from "tlottie/svelte";
</script>
<LottiePlayer src="/animation.json" loop autoplay />Web Component
import "tlottie/webcomponent";<tlottie-player src="/animation.json" loop autoplay></tlottie-player>Vanilla JS
import { createTLottiePlayer } from "tlottie/vanilla";
const { tlottie, destroy } = createTLottiePlayer(document.getElementById("app"), {
src: "/animation.json",
loop: true,
autoplay: true,
});Each adapter also ships a stylesheet for the skeleton shimmer (only needed if you use the outline prop):
import "tlottie/react/style.css"; // or /solid, /vue, /svelte, /vanilla, /webcomponentLoading data
<LottiePlayer src="https://example.com/animation.json" />
<LottiePlayer src="https://example.com/sticker.tgs" /> {/* gzipped, decompressed automatically */}
<LottiePlayer data={jsonString} /> {/* raw Lottie JSON string */}
<LottiePlayer data={uint8ArrayBytes} /> {/* raw bytes, plain or gzipped */}src fetches are cached in-memory per URL and shared across every player instance on the page — loading the same animation twice never re-fetches.
Skeleton / shimmer loading state
Pass a silhouette SVG (as a raw string) via outline; it's rendered as a CSS mask-image behind the canvas until the animation loads (or shown again if it errors):
<LottiePlayer src="/animation.json" outline={outlineSvgString} />Generate that outline SVG from a Lottie/.tgs file with the bundled tlottie-outline CLI — no separate install, works via bunx/npx, or as an npm run script in any project that has tlottie installed:
bunx tlottie-outline --input animation.json
# or: npx tlottie-outline --input animation.json
# writes animation-outline.svg next to itUsage: tlottie-outline --input <file.json|file.tgs> [--output <file.svg>] [--frame <n>] [--size <px>]
-i, --input Path to a Lottie JSON or .tgs (gzipped) file. Required.
-o, --output Path to write the outline SVG. Defaults to <input-without-extension>-outline.svg.
-f, --frame Frame number to trace. Defaults to 0.
-s, --size Raster size (px, square) used for tracing — higher is more accurate and slower. Defaults to 512.It renders the given frame with tlottie's own wasm renderer (same one the library uses), flattens every visible pixel to a black silhouette, and traces that into an optimized SVG path — the same technique, reimplemented, as erfanmola/lottie-output-generator but built on tlottie/wasm instead of thorvg, so it shares this package's gzip decoding and renderer instead of needing its own.
Playback control
Every adapter exposes the underlying TLottie instance (via lottieRefCallback in React/Solid/Svelte, ref+defineExpose in Vue, or the .tlottie property on the custom element / vanilla handle):
tlottie.play();
tlottie.pause();
tlottie.stop();
tlottie.seek(30);
tlottie.setSpeed(1.5);
tlottie.setLoop(true); // or a number of loop repetitions, or false
tlottie.setDirection(-1); // 1 | -1
tlottie.on("load" | "play" | "pause" | "stop" | "frame" | "loopComplete" | "complete" | "error", (payload) => {});speed/loop/direction/fitzModifier props are applied live to the running instance when changed; changing src/data remounts the canvas and reloads.
Configuration
| Prop | Type | Notes |
| ------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------- |
| src / data | string / string \| Uint8Array | One is required. |
| speed | number | Default 1. |
| loop | boolean \| number | true = forever, number = play that many times. |
| direction | 1 \| -1 | |
| autoplay | boolean | Default true. |
| fitzModifier | FitzModifier | Telegram Fitzpatrick skin-tone variant. Parse-time only — changing it recreates the instance. |
| layerColorReplacements | { layerNamePrefix, color }[] | Recolors layers by name prefix. Parse-time only. |
| quality | { antialias?, curveTolerance? } | Render quality knobs. |
| workerCount | number | Spins up a dedicated worker pool of this size just for this player. |
| forceRender | boolean | Keep rendering while off-screen (skips the IntersectionObserver auto-pause). |
| reportFrames | boolean | Emit throttled (~10Hz) frame events, for progress UIs. Off by default (costs a postMessage per emission). |
| playOnClick | boolean | Clicking the canvas calls play(). Mainly for non-looping animations: they play once, then replay on each click. |
Worker pool
By default all players share a single worker (one worker already multiplexes any number of animations fine — each worker owns its own WASM module instance, so sizing the default off navigator.hardwareConcurrency just burns memory on typical multi-core machines for no benefit). Raise it if you've profiled a worker-bound workload:
import { configureTLottie } from "tlottie";
configureTLottie({ workerCount: 4 });Or give one player its own dedicated pool via the workerCount prop.
Eager initialization
By default, the render worker and the wasm binary are both created/fetched lazily — the first Worker spins up when the first player mounts, and the wasm binary isn't requested until that player's animation source has resolved. Call initializeTLottie() any time earlier (module load, route change, hover intent, whatever fits your app) to warm both up ahead of time, so the first real player has nothing left to wait for:
import { initializeTLottie } from "tlottie";
initializeTLottie(); // fire-and-forget is fine
// or: await initializeTLottie({ workerCount: 4, wasmUrl: "/custom/tlottie.wasm" });Every worker in the (grown-to-full-size) pool is warmed, since each worker owns its own wasm module instance. Safe to call more than once or against multiple pools.
Browser support
Requires OffscreenCanvas, requestAnimationFrame inside a dedicated Worker, and (for .tgs) DecompressionStream. All are available in current Chrome/Edge/Firefox/Safari. No fallback path is implemented for older browsers.
Development
This repo vendors tlottie as a git submodule and ships a prebuilt tlottie.wasm — you don't need a Rust toolchain to work on the JS/TS side.
git clone --recurse-submodules https://github.com/erfanmola/tlottie.git
cd tlottie
bun install
bun run dev # demo app at localhost:5173, imports straight from src/
bun run lint # typecheck + biome
bun run build # builds dist/ for every adapterRebuilding src/core/tlottie.wasm from the submodule (only needed after pulling submodule updates or touching the Rust source) requires a Rust toolchain with the wasm32-unknown-unknown target:
bun run build:wasm # regular std build (the shipped default)
bun run build:wasm:no-std # optional no_std build -> src/core/tlottie.no-std.wasmThe shipped package keeps the regular std build; the no_std binary is an opt-in cargo feature (wasm,no-std — allocator via dlmalloc over memory.grow, no libc imports) and is only produced when explicitly requested.
The build uses cargo's release profile (opt-level = 3, full codegen quality) plus a wasm-opt -Oz pass for dead-code elimination and stripping (via the binaryen devDependency, no system install needed) — 488KB → 418KB raw, with no measurable render-speed cost (benchmarked; opt-level = "z" gets smaller still but is a real ~50% slower render path, not worth it here). What actually ships over the wire is smaller still, since fetch() transparently negotiates compression: 163KB gzip, 131KB brotli. Make sure whatever serves dist/tlottie.wasm in production sends Content-Encoding (most CDNs and static hosts do this automatically — a bare/unconfigured dev server might not).
Repo layout
src/core/— WASM loader, memory management, gzip decompression, framework-agnostic playback clocksrc/worker/— the render worker and its worker-poolsrc/main/— main-thread facade (TLottieclass), fetch cache, shimmer helpersrc/{vanilla,webcomponent,react,solid,vue,svelte}/— framework adapterssrc/bin/— thetlottie-outlineCLIdemo/— a page per adapter, exercising load/play/error/gzip/resize
CI
.github/workflows/pages.yml— buildsdemo/and deploys it to GitHub Pages on every push tomain..github/workflows/release.yml— on apackage.jsonversion bump landing onmain, publishes to npm and creates a matching GitHub Release with auto-generated notes. Needs anNPM_TOKENrepo secret (an npm automation token with publish access) to actually publish; without it the workflow fails at the publish step.
License
MIT — see LICENSE. The underlying tlottie engine is also MIT.
