TL: DR - take me to the code
1. Vue 2 Code Sandbox
2. Vue 3 Code Sandbox
3. Difference between Vue 2 and Vue 3 implementation
It is one of those features that slowly but steadily found its way into most of the content-heavy pages, such as blogs or documentations. Scrolling elements into view gives your user an intuitive shortcut to the content they'd like to read about.
Reasons for a Vue directive
You might wonder - why would I need Javascript, even more, a Vue directive for this? I can use built-in HTML and CSS properties, such as href="#headline"
in combination with scroll: smooth
.
In fact, you can. If you don't mind a few limitations:
- A
fixed
navbar might be in the way of your heading - You have to give every heading a fixed id or create it with Javascript
- Globally applied smooth scrolling sometimes leads to some funny side effects when using SPA routers
You could arguably also use vanilla Javascript. Then again, the nature of Vue directives allows for a quick and reusable implementation.
Please note that this article focuses on Vue 3, in which custom directives are handled slightly different than in Vue 2. Check out the linked code sandboxes in TL:DR for differences in
main.js
and thevScroll.js
directive file.
Setup a Vue 3 Project with Vite
We'll use Vite to spin up a basic application. You can alternatively use Vue CLI.
Change into a directory of your choice and type:
# 1: Init your project
npm init vite@latest # using Vite with npm
# yarn create vite # using Vite with yarn
# vue create . # using Vue CLI
# 2: Change into the created folder and start the dev server
cd vite-project
npm install
npm run dev
Make the app object available for directive registration
Before registering our custom directives, let's make a small adjustment in Vue's main file. It uses createApp
on the fly, but we need the created app object to register components on.
This step is optional, you could also chain
.directive()
as part of the createApp bootstrapping process.
// Inside main.js: Change this
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
// ------
// to
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// ... register directives here
app.mount('#app')
With app.directive(directiveName, directiveFunction)
, we're now able to register our own directives everywhere in the app.
Create the scroll directive
For this purpose, we require two browser functions, both of which are available in all major browsers.
-
domElement.getBoundingClientRect()
to receive the element's coordinates -
window.scroll()
to scroll to the calculated part of the window
What's left for us to do is to:
- calculate the difference between the current window position and the element (1)
- make the window scroll to that calculated position (2)
So let's move ahead and create a folder in the /src
folder named /directives
. Inside it, create a file named vScroll.js
and paste the following code into it:
const vScroll = {
mounted: (el) => {
el.style.cursor = 'pointer';
el.addEventListener('click', () => {
const coord = el.getBoundingClientRect().top + window.scrollY; //(1)
window.scroll({ top: coord, behavior: 'smooth' }); //(2)
});
},
};
export default vScroll;
Now let's go back into the main.js
file and register vScroll
:
import { createApp } from 'vue'
import App from './App.vue'
import vScroll from './directives/vScroll'
const app = createApp(App)
app.directive('scroll', vScroll)
app.mount('#app')
Use the directive on the template
Now what's left to do is to check whether the directive works as intended. Let's go ahead and replace the content of the App.vue
file with a Lorem Ipsum template.
Try adding v-scroll
to any of the h2 - tags. Now, when clicking on them, they will be smoothly scrolled to the upper part of the window.
<h2 v-scroll>Cursus mattis</h2>
What about the fixed navbar problem?
The solution in a nutshell: You can bind values to directives! In this case to add a vertical offset when scrolling to an element. These bindings can be accessed the same way as the element itself, given they are passed as an argument in the directive's lifecycle function. In our case, binding.value
will resolve to a number we want to subtract from the scroll-top position.
In the vScroll.js
file:
const vScroll = {
mounted: (el, binding) => {
el.style.cursor = 'pointer';
el.addEventListener('click', () => {
let coord = 0;
coord = binding.value
? el.getBoundingClientRect().top + window.scrollY - binding.value
: el.getBoundingClientRect().top + window.scrollY;
window.scroll({ top: coord, behavior: 'smooth' });
});
},
};
In the HTML template:
<h2 v-scroll="200">
Pharetra magna
</h2>
<p>
Congue quisque egestas diam in arcu cursus euismod quis viverra.
Pharetra magna ac placerat vestibulum lectus mauris ultrices.....
</p>
If everything went well, you now have a functional directive you can include in any new or existing project.
There's more to it
As you can see, we achieved this functionality fairly easily. You can move even further and extend the directive. For example, dynamically add an icon before the text or mutate the element's style whenever a user interacts with it. If you'd like to get an even deeper understanding of Vue directives, make sure to take a look into the official docs for Vue 3.