Nuxt.js adds SSR and auto-imports on top of Vue. Lottie needs the browser environment — window, document, and requestAnimationFrame — none of which exist during server-side rendering. This guide covers every pattern for using Lottie safely in Nuxt 3.
The Core Problem
Nuxt renders HTML on the server. Lottie animations require window. Without SSR guards, you'll see:
ReferenceError: window is not defined
The solution: always initialize Lottie inside onMounted() or use <ClientOnly>.
Before You Start
Open your animation files in IconKing first:
- Preview colors, timing, and loop behavior
- Convert
.json→.lottiefor 75% smaller files - Edit colors to match your Nuxt app's design system
Installation
# Standard lottie-web
npm install lottie-web
# Vue 3 wrapper
npm install vue3-lottie
# dotLottie format
npm install @lottiefiles/dotlottie-vue
Place animation files in public/animations/ — Nuxt serves the public/ directory at the root.
Option 1: ClientOnly + vue3-lottie (Simplest)
Wrap Lottie in Nuxt's built-in <ClientOnly> component to prevent SSR:
<!-- components/LottieHero.vue -->
<script setup lang="ts">
import Vue3Lottie from 'vue3-lottie'
const props = defineProps<{
src: string
width?: number
height?: number
}>()
</script>
<template>
<Vue3Lottie
:animation-link="src"
:width="width ?? 300"
:height="height ?? 300"
loop
:auto-play="true"
/>
</template>
<!-- pages/index.vue -->
<template>
<div>
<h1>Hero Section</h1>
<ClientOnly>
<LottieHero src="/animations/hero.json" :width="400" :height="400" />
</ClientOnly>
</div>
</template>
<ClientOnly> renders nothing during SSR and hydrates on the client — no window is not defined errors.
Option 2: onMounted Guard (Manual lottie-web)
For direct lottie-web control:
<!-- components/LottiePlayer.vue -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import type { AnimationItem } from 'lottie-web'
const props = defineProps<{
src: string
width?: number
height?: number
loop?: boolean
}>()
const container = ref<HTMLDivElement | null>(null)
let anim: AnimationItem | null = null
onMounted(async () => {
const lottie = (await import('lottie-web')).default
if (!container.value) return
anim = lottie.loadAnimation({
container: container.value,
renderer: 'svg',
loop: props.loop ?? true,
autoplay: true,
path: props.src,
})
})
onUnmounted(() => { anim?.destroy() })
defineExpose({ play: () => anim?.play(), pause: () => anim?.pause() })
</script>
<template>
<div ref="container" :style="{ width: `${width ?? 300}px`, height: `${height ?? 300}px` }" />
</template>
Option 3: Nuxt Plugin (.client.ts)
// plugins/lottie.client.ts
import lottie from 'lottie-web'
export default defineNuxtPlugin(() => ({ provide: { lottie } }))
<script setup lang="ts">
const { $lottie } = useNuxtApp()
const container = ref<HTMLDivElement | null>(null)
onMounted(() => {
$lottie.loadAnimation({ container: container.value!, renderer: 'svg', loop: true, autoplay: true, path: '/animations/hero.json' })
})
</script>
The .client.ts suffix ensures this plugin only runs in the browser.
dotLottie in Nuxt (75% Smaller Files)
<script setup lang="ts">
import { DotLottieVue } from '@lottiefiles/dotlottie-vue'
</script>
<template>
<ClientOnly>
<DotLottieVue src="/animations/hero.lottie" loop autoplay style="width:300px;height:300px" />
</ClientOnly>
</template>
Composable: useLottie
// composables/useLottie.ts
export function useLottie(containerRef: Ref<HTMLDivElement | null>, options: { path: string; loop?: boolean }) {
let anim: any = null
onMounted(async () => {
if (!containerRef.value) return
const lottie = (await import('lottie-web')).default
anim = lottie.loadAnimation({ container: containerRef.value, renderer: 'svg', loop: options.loop ?? true, autoplay: true, path: options.path })
})
onUnmounted(() => { anim?.destroy() })
return { play: () => anim?.play(), pause: () => anim?.pause(), stop: () => anim?.stop() }
}
Performance Checklist for Nuxt
| Check | Solution |
|---|---|
window is not defined |
Use <ClientOnly> or onMounted
|
Large .json files |
Convert to .lottie at IconKing
|
| Animation in JS bundle | Dynamic import('lottie-web') inside onMounted
|
| Memory leak on navigation | Always call anim.destroy() in onUnmounted
|
Summary
- Always use
<ClientOnly>oronMounted— Nuxt SSR has nowindow - Use dynamic
import('lottie-web')for smaller initial JS bundles - Put files in
public/animations/and reference by URL - Create a
useLottiecomposable for reuse - The
.client.tsplugin suffix is cleanest for app-wide Lottie setup - Convert to
.lottieat IconKing — 75% smaller files
Top comments (0)