Create an animated Vue 3 component when scrolling into view

Michael Verschoof
7 min readMay 5, 2021
Photo by Paul Skorupskas on Unsplash

These days animation is used a lot in websites. Especially elements that animate when they enter the view have gained massive popularity. It isn’t that difficult to understand why as it looks awesome. The landing page of GitHub is an excellent example of how cool this can be.

“So how can I build my own?”

There are two techniques that, when combined, will allow us to create a reusable component that can be used to animate any content once it enters into the view. So let’s get started!

The setup

You probably have your own project that you’re currently working on, but for this example I’m using the basic Vue 3 project setup using the create Vue app. We will add most of our code to App.vue so you don’t have to switch between components and files too much. The other will be our animated component.

Creating the component

First off, we need to create our component. We’ll create a new file under the components directory called AnimatedComponent.vue. In this file we’ll add the template, the functionality and the styling.

The template

<template>
<div ref="target">
<transition :name="animationType">
<div v-if="animate" class="animated-component">
<slot />
</div>
</transition>
</div>
</template>

As you can see, we don’t need much HTML for this component. It consists of an element with ref="target" that will be used to determine if the element is inside our view. I’ll get back to this later.

Next a Vue transition component is used. This is part of Vue itself so it doesn’t need to be imported or anything. What this does is add certain classes to the element inside it to trigger the animations. We’ll get back to this some more in a while.

Inside the transition an element has to be present that uses v-if or v-show (or a custom directive, see the bonus chapter at the end of the story). The transition component uses this to determine whether it has to trigger it’s animation states. Without it, nothing will happen. See Vue’s documentation for more information about this.

Lastly, a Vue slot is added. This will insert the content that we’ll add in the App.vue code between the <animated-code> tags. If you’re not yet familiar with Vue slots, you should definitely check this out as this is a very useful and powerful bit of functionality in the Vue framework.

The functionality

The above template doesn’t do anything useful at this moment, so let’s remedy that by adding some functionality. We’ll add a script tag containing the Vue 3 component code above the template.

<script setup lang="ts">
import { onMounted, ref } from 'vue';

withDefaults(defineProps<{ animationType?: string }>(), { animationType: 'fade' });

const target = ref<Element>();
const animate = ref<boolean>(false);

const observer = new IntersectionObserver(
([entry]) => {
animate.value = entry.isIntersecting;
},
{
threshold: 0.5
}
);

onMounted(() => {
observer.observe(target.value as Element);
});
</script>

This component takes a property called animationType which is an optional string that defaults to the “fade” animation. This will be added later in the CSS. It is surrounded by a withDefaults function that sets a default value to the optional property. Documentation about this can be found here.

Next we’ve added an IntersectionObserver. Simply put; this watches the element defined with the ref="target" in the template and watches if it is inside the view. The threshold option determines that at least 50% of the element needs to be inside the view in order to trigger the callback function. The callback function in turn sets a boolean variable in order to trigger the animation.

For more information about how the observer works you can check out my previous story and/or the Mozilla developer documentation.

Adding the styling

Now that we have added the functionality that will trigger the animation of our component we have to add the actual CSS animations. In this example we’re adding two different animations: fade and zoom.

<style scoped>
.animated-component.fade-enter-from,
.animated-component.zoom-enter-from {
transition: none;
}

/* Fade animation */
.fade-enter-active,
.fade-leave-active {
transition: opacity 300ms ease;
}

.fade-enter-from,
.fade-leave-to {
opacity: 0;
}

/* Zoom animation */
.zoom-enter-active,
.zoom-leave-active {
transition: transform 300ms ease;
}

.zoom-enter-from,
.zoom-leave-to {
transform: scale(0.9);
}
</style>

The transition component adds classes for different states within the animation. This is based on the name property that is provided. For example; zoom-enter-active is used when the component is entering the view when the component has the property name set to “zoom”. You can check out the different states in the Vue documentation.

Adding the component to the application

Now that we’ve created our component, we can add it to the page so we can see it in action.

Import the component

The App.vue does not contain our newly created component yet. So we’ll import it in the script tag in App.vue.

<script setup lang="ts">
import AnimatedComponent from '@/components/AnimatedComponent.vue';
import { RouterLink, RouterView } from 'vue-router';
import HelloWorld from './components/HelloWorld.vue';
</script>

Add the component to the template

Next we’ll use the imported component in the template. This will use the <animation-component> tags in which you can place your own content. For this example we’ll add an element containing some Lorem Ipsum as a placeholder.

<template>
<header>
<img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />

<div class="wrapper">
<HelloWorld msg="You did it!" />

<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
</nav>
</div>
</header>

<RouterView />

<div class="demo-container">
<animated-component>
<div class="demo">
Lorem ipsum dolor...
// The rest of the text is omitted for readability
</div>
</animated-component>

<animated-component animation-type="zoom">
<div class="demo">
Lorem ipsum dolor...
// The rest of the text is omitted for readability
</div>
</animated-component>
</div>
</template>

As you can see, the first component has no animation type attribute provided to it, which causes the default value “fade” to be used. The second one has the type “zoom”, triggering a different animation.

We can actually test the implementation now by going to the page and refreshing. The components should now fade and zoom in respectively. But as they have no styling at all it may be less visible and just looks bad.

Add some styling to the content

To make the animations easier to spot we’ll add some styling to the content. We’ve already added the class “demo” to the component’s content which we’ll use here. By adding the following styling to the existing style tag the components will be easier to spot.

.demo {
color: white;
max-width: 600px;
border: 2px solid #9dd3ba;
padding: 2rem;
margin: 2rem auto;
background-color: #42b983;
}

It’s not the prettiest of components but it will do the trick for demonstration purposes. When you refresh the page you’ll see the components being animated, showing us that the functionality works.

Testing if it works when scrolling into view

As the title said we want this to work when the components scroll into view we’ll have to adjust the styling of the page a bit to make sure it scrolls. In this example we’ve added a container element around the two animated components. We just need to add some padding to the container. So in the same style tag, add the following CSS:

.demo-container {
min-height: 100vh;
padding: 50vh 0;
}

Now our testing page will have the components outside the initial view and when scrolling down, you’ll see the elements being animated into view!

Conclusion

That’s it! We’ve created a reusable component that will animate any content once it is inside the view. This can be used for images, video’s and any other content that you can think of.

Getting the code

The result code of this tutorial can be found in GitHub. This also includes the code from the below bonus chapter.

Bonus: Keeping the space occupied

A downside of the default v-if and v-show functionality is that they remove the element from the DOM or set the display to none. In both cases the size of the page is impacted if you don’t have a containing element that keeps the space intact. This can cause the page to “jump” as the animated component is being shown or hidden.

Creating a custom directive

In order to fix this caveat, we can create a custom directive that does not remove the element from the view but instead sets the visibility of the element. This means the space in the page will still be occupied by the element so the structure is not impacted.

In main.ts we’ll create the directive and add it to the application so we can use it in our template.

import { createPinia } from 'pinia';
import {
createApp, type Directive, type DirectiveBinding, type VNode
} from 'vue';
import './assets/main.css';

import App from './App.vue';
import router from './router';

export const appear: Directive = {
beforeMount(element: HTMLElement) {
element.style.visibility = 'hidden';
},
updated(
element: HTMLElement,
binding: DirectiveBinding<boolean>,
node: VNode
) {
if (!binding.value === !binding.oldValue || null === node.transition) {
return;
}

if (!binding.value) {
node.transition.leave(element, () => {
element.style.visibility = 'hidden';
});
return;
}

node.transition.beforeEnter(element);
element.style.visibility = '';
node.transition.enter(element);
}
};

const app = createApp(App);

app.use(createPinia());
app.use(router);
app.directive('appear', appear);

app.mount('#app');

This directive uses some Vue states in order to first set the visibility to “hidden” as an initial value. Then it checks if it needs to add or remove the value in the updated state. Lastly the directive is added to the app variable that was created by the createApp() function so it will be available in the templates.

Use the directive

The last thing is to replace the used directive in AnimatedComponent.vue.

<template>
<div ref="target">
<transition :name="animationType">
<div v-appear="animate" class="animated-component">
<slot />
</div>
</transition>
</div>
</template>

Now the component will show or hide while leaving the rest of the page unaffected. Cool, right?

--

--

Michael Verschoof

Ex-Java developer turned Javascript / Typescript and Vue developer at BTC Direct Europe B.V.