DEV Community

Fazal Shah
Fazal Shah

Posted on

Lottie Animations in Nuxt.js: A Complete Guide

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
Enter fullscreen mode Exit fullscreen mode

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.lottie for 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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
<!-- pages/index.vue -->
<template>
  <div>
    <h1>Hero Section</h1>
    <ClientOnly>
      <LottieHero src="/animations/hero.json" :width="400" :height="400" />
    </ClientOnly>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

<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>
Enter fullscreen mode Exit fullscreen mode

Option 3: Nuxt Plugin (.client.ts)

// plugins/lottie.client.ts
import lottie from 'lottie-web'
export default defineNuxtPlugin(() => ({ provide: { lottie } }))
Enter fullscreen mode Exit fullscreen mode
<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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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() }
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Always use <ClientOnly> or onMounted — Nuxt SSR has no window
  2. Use dynamic import('lottie-web') for smaller initial JS bundles
  3. Put files in public/animations/ and reference by URL
  4. Create a useLottie composable for reuse
  5. The .client.ts plugin suffix is cleanest for app-wide Lottie setup
  6. Convert to .lottie at IconKing — 75% smaller files

Top comments (0)