Easily add custom styling to a menu in Vue 3 when it is “sticking”
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.
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.