our application.
Implementation:
// src/components/AnimatedAsset.vue
<script setup lang="ts">
import { DotLottieVue } from '@lottiefiles/dotlottie-vue'
import type { DotLottieVueProps } from '@lottiefiles/dotlottie-vue'
interface Props {
src: string
loop?: boolean
speed?: number
ariaLabel?: string
}
const props = withDefaults(defineProps<Props>(), {
loop: true,
speed: 1,
ariaLabel: 'Animation'
})
</script>
<template>
<div class="lottie-wrapper" :style="{ aspectRatio: '1 / 1' }">
<DotLottieVue
:src="props.src"
:loop="props.loop"
:autoplay="true"
:speed="props.speed"
:aria-label="props.ariaLabel"
role="img"
/>
</div>
</template>
<style scoped>
.lottie-wrapper {
display: inline-flex;
overflow: hidden;
}
</style>
Architecture Rationale:
- Wrapper Encapsulation: By wrapping
DotLottieVue, you centralize configuration. If the underlying library updates its API, you only update one file.
- Accessibility: The wrapper injects
role="img" and aria-label, ensuring animations are accessible to screen readers, a common omission in raw implementations.
- Layout Stability: The container enforces an aspect ratio, preventing Cumulative Layout Shift (CLS) during asset loading.
2. lottie-web: Composable Pattern
When using lottie-web, the imperative nature of the library conflicts with Vue's declarative model. A composable isolates the lifecycle management and exposes a clean reactive interface.
Composable Implementation:
// src/composables/useVectorAnimation.ts
import { ref, onMounted, onBeforeUnmount, type Ref } from 'vue'
import lottie from 'lottie-web'
import type { AnimationItem, AnimationConfig } from 'lottie-web'
export function useVectorAnimation(
containerRef: Ref<HTMLElement | null>,
config: Omit<AnimationConfig, 'container'>
) {
const instance = ref<AnimationItem | null>(null)
onMounted(() => {
if (containerRef.value) {
instance.value = lottie.loadAnimation({
...config,
container: containerRef.value,
})
}
})
onBeforeUnmount(() => {
instance.value?.destroy()
instance.value = null
})
return {
instance,
play: () => instance.value?.play(),
pause: () => instance.value?.pause(),
stop: () => instance.value?.stop(),
}
}
Usage Example:
<!-- src/components/InteractionTrigger.vue -->
<template>
<div
ref="animContainer"
class="trigger-zone"
@click="handleInteraction"
/>
<p v-if="isComplete">Action completed!</p>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useVectorAnimation } from '@/composables/useVectorAnimation'
import successData from '@/assets/animations/success.json'
const animContainer = ref<HTMLDivElement | null>(null)
const isComplete = ref(false)
const { instance, play } = useVectorAnimation(animContainer, {
animationData: successData,
loop: false,
autoplay: false,
renderer: 'svg',
})
function handleInteraction() {
if (instance.value) {
isComplete.value = false
play()
instance.value.addEventListener('complete', () => {
isComplete.value = true
}, { once: true })
}
}
</script>
Architecture Rationale:
- Lifecycle Safety: The composable guarantees
destroy() is called in onBeforeUnmount, preventing the memory leaks common in ad-hoc lottie-web usage.
- Reusability: The logic is decoupled from the UI, allowing the same animation logic to be applied to different containers or triggered by different events.
- Type Safety: Using
Omit<AnimationConfig, 'container'> ensures the container is managed internally, preventing configuration errors.
Pitfall Guide
Production environments expose subtle issues that often go unnoticed during development. The following pitfalls address common failure modes and their remedies.
-
Memory Leaks in Imperative Libraries
- Explanation:
lottie-web creates DOM nodes and event listeners that persist even after the Vue component unmounts if not explicitly destroyed. This leads to increasing memory usage and potential crashes in SPAs.
- Fix: Always pair
lottie.loadAnimation with anim.destroy() in the component's teardown hook. Use the composable pattern provided above to automate this.
-
Cumulative Layout Shift (CLS)
- Explanation: Animations often load asynchronously. If the container has no defined dimensions, the layout shifts when the animation renders, degrading Core Web Vitals.
- Fix: Set explicit
width and height or use aspect-ratio on the container. The wrapper component example includes this safeguard.
-
Bundle Bloat from Redundant Imports
- Explanation: Importing
lottie-web pulls in the entire renderer engine, including SVG and Canvas renderers, even if you only use one. This adds ~500KB to your bundle.
- Fix: Prefer
@lottiefiles/dotlottie-vue for standard use cases. If lottie-web is required, analyze your bundle to ensure tree-shaking is effective, though the runtime size remains higher than DotLottie.
-
Reactivity Conflicts
- Explanation: Passing reactive Vue objects directly into
lottie-web configuration can cause unexpected re-initialization or performance degradation, as the library may attempt to traverse reactive proxies.
- Fix: Use
toRaw() from Vue when passing data to imperative libraries, or ensure configuration objects are static. The composable pattern helps by isolating the config.
-
Accessibility Neglect
- Explanation: Decorative animations can interfere with screen readers or distract users with vestibular disorders.
- Fix: Add
aria-hidden="true" to purely decorative animations. For meaningful animations, provide aria-label and respect prefers-reduced-motion media queries by disabling autoplay or reducing speed.
-
Format Mismatch and Bandwidth Waste
- Explanation: Using the DotLottie loader but serving raw
.json files negates the compression benefits. Conversely, trying to load .lottie files with lottie-web will fail.
- Fix: Standardize on the
.lottie format for all assets when using @lottiefiles/dotlottie-vue. Convert existing JSON assets using official tools to achieve the ~80% size reduction.
-
Event Listener Accumulation
- Explanation: Attaching event listeners (e.g.,
complete, loopComplete) inside a loop or without cleanup can cause handlers to fire multiple times.
- Fix: Use the
{ once: true } option for one-time events, or explicitly remove listeners in the teardown phase. The interaction example demonstrates the once pattern.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Standard UI Icons / Loaders | @lottiefiles/dotlottie-vue | Minimal runtime, Vue-native API, automatic lifecycle management. | Low (Bundle & Dev Time) |
| Complex Hero Animations | @lottiefiles/dotlottie-vue | Supports compressed .lottie format, reducing load times for large assets. | Low (Bandwidth) |
| Granular Frame Control | lottie-web | Required for custom segment playback, frame-accurate scrubbing, or custom renderers. | High (Bundle Size) |
| Strict Bundle Budget | @lottiefiles/dotlottie-vue | ~100KB runtime vs ~500KB; critical for performance-sensitive apps. | Low (Performance) |
| Legacy Codebase Migration | lottie-web | Easier incremental migration if existing imperative logic cannot be refactored immediately. | Medium (Tech Debt) |
Configuration Template
For Nuxt or large-scale Vue applications, register the DotLottie component globally to reduce boilerplate.
// plugins/lottie.global.ts
import { DotLottieVue } from '@lottiefiles/dotlottie-vue'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.component('LottieAsset', DotLottieVue)
})
Usage in templates becomes declarative and concise:
<template>
<LottieAsset
src="/assets/hero.lottie"
:loop="true"
:speed="0.8"
aria-label="Welcome animation"
/>
</template>
Quick Start Guide
- Install the Package: Run
npm install @lottiefiles/dotlottie-vue in your project root.
- Prepare Assets: Convert your animation files to
.lottie format using the official Lottie converter tools.
- Create Wrapper: Copy the
AnimatedAsset.vue wrapper component into your components directory.
- Integrate: Import and use the wrapper in your views, passing the
src prop to your .lottie file.
- Verify: Check your bundle size and network tab to confirm the reduced payload and successful rendering.