Article: May 12, 2024

Scroll Animation with Vue 3 and VueUse

hamza ahmad - frontend web developer

@hamzahmd

Animations and Transitions are meant to invoke life on the web pages. Typically, animations occur through user interactions with the website, whether it be clicking a button, hovering over an element, or scrolling through the viewpoint.

Scroll Animation with Vue 3 and VueUse

There are some specialized animation libraries (like GSAP) on the market, but here we are implementing scroll transition with Vue 3 using composition API and VueUse components which is a lightweight solution. Through this approach, we'll also learn how the specific element is activated when the window scroll reaches that point, opening endless opportunities to perform animations.

Installation

We only need the following three tools:

  • vue (v3)
  • tailwindcss (optional)
  • @vueuse/core
  1. Let's start with a fresh codebase. You can clone the repository if you prefer. I'm using npm, but you can also use yarn or pnpm.
npm create vite@latest vue-scroll-transition -- --template vue
// or with TypeScript
npm create vite@latest vue-scroll-transition -- --template vue-ts

cd my-project
  1. Next we need to install tailwind and its peer dependencies. Then generate tailwind.config.js and postcss.config.js files.
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Please follow the remaining configuration setups for Tailwind CSS on their official guidelines. They're pretty straightforward.

  1. Now, install VueUse library. VueUse is a collection of utility functions based on the Composition API.
npm i @vueuse/core

Reusable Component

Now that we've installed all the required dependencies, we can start building our reusable component that we can use anywhere. For the sake of simplicity, let's name it ScrollTransition.vue:

Basic Example

We're utilizing the useElementVisibility composable utility. It tracks the visibility of an element within the viewport. For that, we just need to add the ref to that element. Then, whenever we reach that component, we'll be able to get the true value; otherwise, the value will be false. Let's test it with the basic example of opacity (fade-in & fade-out) using Tailwind CSS:

<template>
  <div
  ref="el"
  :class="[
    'transition-all duration-500',
    isVisible ? 'opacity-100' : 'opacity-0'
    ]"
  >
    <slot />
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useElementVisibility } from '@vueuse/core'

const el = ref(null)
const isVisible = useElementVisibility(el)
</script>

VueUse is indeed awesome! Since we're using slots, it can take any HTML element or other components.

Single Scroll Animation

Minimize excessive transitions by triggering the animation only once when scrolling reaches the specific element.

<template>
  <div
  ref="el"
  :class="[
    'transition-all duration-700',
    trueCount < 1 && isVisible ? 'opacity-100' : 'opacity-0'
    ]"
  >
    <slot />
  </div>
</template>

<script setup>
import { ref, watchEffect, unref } from 'vue'
import { useElementVisibility } from '@vueuse/core'

const el = ref(null)
const isVisible = useElementVisibility(el)
const trueCount = ref(0)

watchEffect(() => {
  if (unref(isVisible) === true && unref(trueCount) < 1) {
    trueCount.value++
  }
})
</script>

Slding Effect

We've reached the point of completing this reusable component. Let's utilize the composition API by defining props for the component.

<template>
  <div
  ref="el"
  :class="[
      'transition-all duration-700',
      moreDelay ? 'delay-700' : 'delay-300',
      trueCount < 1 && translateEffect
        ? isVisible
          ? transtionPosition[transition].from
          : transtionPosition[transition].to
        : '',
      trueCount < 1 && opacityEffect
        ? isVisible
          ? 'opacity-100'
          : 'opacity-0'
        : '',
    ]"
  >
    <slot />
  </div>
</template>

<script setup>
import { ref, watchEffect, unref } from 'vue'
import { useElementVisibility } from '@vueuse/core'

const el = ref(null)
const isVisible = useElementVisibility(el)
const trueCount = ref(0)

watchEffect(() => {
  if (unref(isVisible) === true && unref(trueCount) < 1) {
    trueCount.value++
  }
})

const transtionPosition = = {
  vertical: { from: 'translate-y-0', to: 'translate-y-12' },
  horizontalToRight: { from: '-translate-x-0', to: '-translate-x-12' },
  horizontalToLeft: { from: 'translate-x-0', to: 'translate-x-12' },
}

defineProps([
  'transition',
  'moreDelay',
  'translateEffect',
  'opacityEffect'
])
</script>

Here, we can also specify the HTML tag name, such as:

<scroll-transition as='li'/>

With TypeScript

Let's take it to the next level with TypeScript to ensure type safety and enable autocompletion. Here's our final code:

<template>
  <component
    :is="as"
    ref="el"
    :class="[
      'transition-all duration-700',
      moreDelay ? 'delay-700' : 'delay-300',
      trueCount < 1 && translateEffect
        ? isVisible
          ? transtionPosition[transition].from
          : transtionPosition[transition].to
        : '',
      trueCount < 1 && opacityEffect
        ? isVisible
          ? 'opacity-100'
          : 'opacity-0'
        : '',
    ]"
  >
    <slot />
  </component>
</template>

<script setup lang="ts">
import { ref, watchEffect } from 'vue'
import { useElementVisibility } from '@vueuse/core'

type ObjectKeyPair = {
  [key: string]: {
    [key: string]: string
  }
}

const el = ref(null)
const isVisible = useElementVisibility(el)
const trueCount = ref(0)

watchEffect(() => {
  if (isVisible.value && trueCount.value < 1) {
    trueCount.value++
  }
})

const transtionPosition: ObjectKeyPair = {
  vertical: { from: 'translate-y-0', to: 'translate-y-12' },
  horizontalToRight: { from: '-translate-x-0', to: '-translate-x-12' },
  horizontalToLeft: { from: 'translate-x-0', to: 'translate-x-12' },
}

type TransitionTypes = {
  translateEffect?: boolean
  opacityEffect?: boolean
  moreDelay?: boolean
  transition?: 'vertical' | 'horizontalToRight' | 'horizontalToLeft'
  as?: string
}

withDefaults(defineProps<TransitionTypes>(), {
  translateEffect: true,
  opacityEffect: true,
  moreDelay: false,
  transition: 'vertical',
  as: 'div',
})
</script>

That's it. Feel free to integrate it into your projects. Here is a quick demo. You can customize it with as many animations as you'd like.

If you find this code snippet useful, please consider giving a star to the code repository.

To ask questions about the article, please comment directly on this Linkedin post.

Interested to Collaborate?

Let's explore customized solutions for your product.