Easily add custom styling to a menu in Vue 3 when it is “sticking”

Michael Verschoof
6 min readApr 26, 2021
Photo by AbsolutVision on Unsplash

Vue 3 is a new and exciting framework for building websites and web applications. As it is new, you’ll run into some specific things you would like to add but are unsure on how to implement.

A common example for this is adding styling to a menu once it “sticks” to the top of your page. I’ve ran into the same thing and it took me a while to figure it out so I’ll show you how to do it. It’s a lot simpler than I expected!

The setup

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

Adding the HTML and CSS

In order to see our example in action, we need to add an element and styling to the default page. This will make sure the page can scroll and we have an element that sticks to the top of the page when scrolling.

Make sure the page scrolls

First we’ll add a bit of CSS to the app to make it higher than our screen to enable scrolling.

<style scoped>
.enable-scrolling {
margin-top: 120vh;
margin-bottom: 50vh;
}

// The default styling that was already there

We’ll surround the existing HTML in the page with a div that uses the added class.

<template>
<div class="enable-scrolling">
<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>
</template>

Add the sticky menu

Next we’ll create an element that will function as our sticky menu. We’ll add the element above the existing navigation so it sticks once you’ve scrolled a bit in order to see the differences between sticking and not sticking.

<template>
<div class="sticky-menu">This is our sticky menu</div>

<div class="enable-scrolling">
<header>

// The rest of the template

Of course the newly added element isn’t sticking yet, so we’ll add some CSS in order to make it sticky. I’ve added the background color and padding in order to make it look better in this example.

<style scoped>
.sticky-menu {
background-color: #41b883;
padding: 1rem;
position: sticky;
top: 0;
color: white;
z-index: 1;
grid-column-start: 1;
grid-column-end: 3;
}

.enable-scrolling {
margin-top: 120vh;
margin-bottom: 50vh;
}

// The default styling that was already there

So far, so good. We now have an element that sticks to the top of the page when scrolling down past it.

The z-index, grid-column-start and grid-column-end are added as the default layout of the page messes things up otherwise.

The actual functionality

Now for the interesting part, actually doing something with the menu once it is sticking to the top of the page.

Preparing the template

In order to get the below functionality working, we need a reference in our template that the Vue code can target later on. I’ll explain later what this is actually used for.

<template>
<div class="sticky-menu">This is our sticky menu</div>

<div class="enable-scrolling" ref="target">

// The rest of the template

The Intersection Observer

The Intersection Observer is a JavaScript API designed to trigger functionality when its target element is (partly) inside or outside the viewport. This may sound scary but it enables us to perform actions once an element (partly) enters or leaves the view. The documentation of Mozilla is actually quite good so I’ll leave it to them to explain in more detail.

Adding the observer

We’ll replace the current <script> tag with the below code. This adds the necessary functionality which is described below.

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

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

const observer = new IntersectionObserver(
([entry]) => {
console.log(entry);
},
{ threshold: 0.0 }
);

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

What happens in this code is a “target” element is defined. As this is also returned in the return of the function, it automagically binds to the HTML element where we added the ref="target" to.

After this the actual observer is defined. It takes two parameters of which the first is a callback function that is triggered when the observer triggers. At this time it only logs the entry to the console where you’ll be able to check out what this object contains. The second parameter is an options object where we only set the threshold at this time. By setting this to zero, the callback function is triggered every time the target element enters or leaves the view.

Lastly, we tell the observer to actually start observing the target element. We do this in the onMounted() hook of the component as the target element does not exist in the DOM before then. This has to do with Vue’s render cycles with which I’m not going to bore you today.

Adding the styling to the menu

Now we have functionality that triggers every time that the target element enters or leaves the view, we can use that to add styling to the menu. Let’s first add the CSS class itself.

.sticky-menu.sticking {
background-color: rgba(65, 184, 131, 0.3);
box-shadow: 0 8px 12px 0 rgba(0, 0, 0, 0.3)
}

Now we’ll add a variable that is set to true when the target element is outside the view and use that to add the above CSS class to our menu. This is done by checking the isIntersecting value of the entry object which sets our variable to true if the target element is outside the view.

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

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

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

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

The only thing remaining is adding a class binding to our menu.

<div class="sticky-menu" :class="{ sticking }">
This is our sticky menu
</div>

This will add the CSS class sticking to our element if the sticking variable in the JavaScript is set to true, updating the background color and adding a shadow to the element.

Checking the result

Now when you scroll down the page, it will look like this. It isn’t pretty, but it demonstrates the functionality, leaving you do improve the styling as you wish.

The menu changes color to semi-transparent and gets a shadow when scrolling

Conclusion

That’s it! We’ve added custom styling to our menu based on the position of our target element in regards to our view. When the target element is outside the view the menu becomes semi-transparent and gets a shadow.

That wasn’t as scary as it looked, now was it?

Looking forward

Using this technique we’ll be able to trigger animations, lazy load images and other neat CSS tricks when an element enters or leaves the view.

I’ve written a story on how to create a reusable component that animates when scrolling into view. Curious about how to easily create this? Check it out here.

Getting the code

You can get the result of this tutorial at my GitHub. All the relevant code has been kept in App.vue for an easier overview.

--

--

Michael Verschoof

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