> For the complete documentation index, see [llms.txt](https://docs.ecwid.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ecwid.com/site-themes/develop-site-themes/sections/lottie-animations.md).

# Lottie animations

This guide explains how to add **Lottie animations** to your custom Crane sections using the same pattern as `vape_template/cover-section` section template.

### Overview

The pattern has three parts:

1. **The animation JSON file** — placed in the section's `assets/` folder.
2. **The `useLottieAnimationLocal` composable** — handles lazy-loading of both the library and the animation data, and manages the animation lifecycle.
3. **A client/server component split** — the actual animation lives in a client-only component; the server counterpart renders an empty placeholder to avoid SSR mismatches.

### 1. Add dependency

`lottie-web` must be listed as a direct dependency in your theme's `package.json`:

```json
"dependencies": {
  "lottie-web": "^5.13.0"
}
```

### 2. Place animation file

Put your Lottie JSON file inside the section's `assets/` folder:

```
sections/
  my-section/
    assets/
      my-animation.json   ← here
```

The animation file can be exported from After Effects (via Bodymovin) or any other Lottie-compatible tool.

### 3. Add composable file

Create a new file and put it in your project like this: `sections/my-section/utils/use-lottie-animation-local.ts`:

```typescript
import type { AnimationItem } from 'lottie-web';
import { ref, onMounted, onUnmounted, watch, type ComputedRef } from 'vue';

const IDLE_FALLBACK_DELAY_MS = 200;
const IDLE_TIMEOUT_MS = 2000;

const scheduleIdle = (fn: () => void): void => {
  if (typeof window === 'undefined') return;
  if ('requestIdleCallback' in window) {
    window.requestIdleCallback(fn, { timeout: IDLE_TIMEOUT_MS });
  } else {
    setTimeout(fn, IDLE_FALLBACK_DELAY_MS);
  }
};

export const useLottieAnimationLocal = (
  loadAnimationData: () => Promise<object>,
  showAnimation: ComputedRef<boolean>,
) => {
  const lottieContainer = ref<HTMLElement>();
  const lottieAnimationInitialized = ref(false);
  let lottieInstance: AnimationItem | null = null;

  const initializeLottieAnimation = async () => {
    if (lottieContainer.value && !lottieAnimationInitialized.value) {
      try {
        const [{ default: lottie }, animationData] = await Promise.all([
          import('lottie-web'),
          loadAnimationData(),
        ]);

        if (!lottieContainer.value) return;

        lottieInstance = lottie.loadAnimation({
          container: lottieContainer.value,
          renderer: 'svg',
          loop: true,
          autoplay: true,
          animationData,
          rendererSettings: {
            preserveAspectRatio: 'xMidYMid slice',
          },
        });

        lottieAnimationInitialized.value = true;
      } catch (error) {
        if (process.env.NODE_ENV === 'development') {
          console.warn('Failed to initialize Lottie animation:', error);
        }
      }
    }
  };

  watch(showAnimation, (newValue) => {
    if (newValue && !lottieAnimationInitialized.value) {
      scheduleIdle(initializeLottieAnimation);
    }
  });

  onMounted(() => {
    if (showAnimation.value) {
      scheduleIdle(initializeLottieAnimation);
    }
  });

  onUnmounted(() => {
    lottieInstance?.destroy();
  });

  return { lottieContainer };
};
```

**Notes:**

* **Why `requestIdleCallback`?**\
  Loading and decoding a Lottie JSON (which can be 1–2 MB) is expensive. Deferring it to idle time prevents it from competing with the hero image for bandwidth and main-thread time during LCP.
* **Why dynamic `import('lottie-web')`?**\
  The library is only needed on the client and only when the animation is actually shown. Dynamic import keeps it out of the initial JS bundle.

### 4. Add `sectionAssetUrl` helper

Similarly, create a new file here: `shared/utils/section-asset-url.ts`:

```typescript
export function sectionAssetUrl(filename: string, moduleUrl: string): string {
  let base = moduleUrl;
  const clientIdx = base.lastIndexOf('/client/');
  if (clientIdx !== -1) {
    base = base.slice(0, clientIdx);
  }
  base = base.replace(/\/js\/main$/, '');
  return `${base}/assets/${encodeURIComponent(filename)}`;
}
```

Then import and use it in any component that needs to fetch a section asset:

```typescript
import { sectionAssetUrl } from '../../../shared/utils/section-asset-url';

async function loadAnimationData() {
  const url = sectionAssetUrl('my-animation.json', import.meta.url);
  const response = await fetch(url);
  return response.json();
}
```

**Important:** always pass `import.meta.url` from the **calling component file**, not from inside a shared helper. Rollup can move shared chunks to a different path, so reading `import.meta.url` inside the helper would resolve to the wrong location.

### 5. Add client and server components

Create a new file for the **client component** in `sections/my-section/components/AnimatedBackground.vue`:

```vue
<template>
  <div v-show="show" class="my-section__animated-bg">
    <div ref="lottieContainer" class="my-section__lottie-animation"></div>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue';
import { sectionAssetUrl } from '../../../shared/utils/section-asset-url';
import { useLottieAnimationLocal } from '../utils/use-lottie-animation-local';

interface Props {
  show: boolean;
}

const props = defineProps<Props>();

async function loadAnimationData() {
  const url = sectionAssetUrl('my-animation.json', import.meta.url);
  const response = await fetch(url);
  return response.json();
}

const showAnimation = computed(() => props.show);
const { lottieContainer } = useLottieAnimationLocal(loadAnimationData, showAnimation);
</script>
```

And a file for the **server component** in:\
`sections/my-section/components/AnimatedBackgroundServer.vue`.

**Note:** The server component is a no-op placeholder. It must exist to prevent SSR hydration mismatches — the client component must not render on the server.

```vue
<template>
  <div v-show="show" class="my-section__animated-bg">
    <div class="my-section__lottie-animation"></div>
  </div>
</template>

<script setup lang="ts">
interface Props {
  show: boolean;
}
defineProps<Props>();
</script>
```

You also need to import new components in your client/server section files:

```vue
<!-- MyClient.vue -->
<script setup lang="ts">
import AnimatedBackground from './components/AnimatedBackground.vue';
</script>

<!-- MyServer.vue -->
<script setup lang="ts">
import AnimatedBackground from './components/AnimatedBackgroundServer.vue';
</script>
```

Both use `<AnimatedBackground :show="showAnimation" />` in the template.

### 6. Add a CMS toggle (optional)

If you want the end user to be able to enable/disable the animation in the theme editor, add a `TOGGLE` setting in your section's content file `sections/my-section/settings/content.ts`:

```typescript
showAnimatedBackground: {
  type: 'TOGGLE',
  label: '$label.show_animated_background.label',
  description: '$label.show_animated_background.description',
  defaults: {
    enabled: true,
  },
},
```

Then derive a computed prop from it and pass it to the component:

```typescript
const showAnimation = computed(() => settings.showAnimatedBackground.enabled);
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.ecwid.com/site-themes/develop-site-themes/sections/lottie-animations.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
