112

I want to listen to the window scroll event in my Vue component. Here is what I tried so far:

<my-component v-on:scroll="scrollFunction">
    ...
</my-component>

With the scrollFunction(event) being defined in my component methods but it doesn't seem to work.

Anyone has any idea how to do this?

Thanks!

jeerbl
  • 7,537
  • 5
  • 25
  • 39
  • Possible duplicate of [Add Vue.js event on window](https://stackoverflow.com/questions/36993834/add-vue-js-event-on-window) – xianshenglu Jun 03 '18 at 09:59
  • See https://forum.vuejs.org/t/for-v-on-aspecificevent-is-it-necessary-to-create-destroy-event-listeners/40688/8: "A custom component, as just a calling syntax beacon, is not a component that can emit an event but can forward an event from within it. Only native components can emit a native event." – jrc Nov 16 '19 at 21:37
  • In vue3, this works out of the box. No need for any additional code. – Edward Gaere Dec 12 '22 at 18:50

13 Answers13

241

Actually I found a solution. I'm adding an event listener on the scroll event when the component is created and removing the event listener when the component is destroyed.

export default {
  created () {
    window.addEventListener('scroll', this.handleScroll);
  },
  unmounted () {
    window.removeEventListener('scroll', this.handleScroll);
  },
  methods: {
    handleScroll (event) {
      // Any code to be executed when the window is scrolled
    }
  }
}
jeerbl
  • 7,537
  • 5
  • 25
  • 39
50

In my experience, using an event listener on scroll can create a lot of noise due to piping into that event stream, which can cause performance issues if you are executing a bulky handleScroll function.

I often use the technique shown here in the highest rated answer, but I add debounce on top of it, usually about 100ms yields good performance to UX ratio.

Here is an example using the top-rated answer with Lodash debounce added:

import debounce from 'lodash/debounce';

export default {
  methods: {
    handleScroll(event) {
      // Any code to be executed when the window is scrolled
      this.isUserScrolling = (window.scrollY > 0);
      console.log('calling handleScroll');
    }
  },

  mounted() {
    this.handleDebouncedScroll = debounce(this.handleScroll, 100);
    window.addEventListener('scroll', this.handleDebouncedScroll);
  },

  beforeDestroy() {
    // I switched the example from `destroyed` to `beforeDestroy`
    // to exercise your mind a bit. This lifecycle method works too.
    window.removeEventListener('scroll', this.handleDebouncedScroll);
  }
}

Try changing the value of 100 to 0 and 1000 so you can see the difference in how/when handleScroll is called.

BONUS: You can also accomplish this in an even more concise and reuseable manner with a library like vue-scroll. It is a great use case for you to learn about custom directives in Vue if you haven't seen those yet. Check out https://github.com/wangpin34/vue-scroll.

This is also a great tutorial by Sarah Drasner in the Vue docs: https://v2.vuejs.org/v2/cookbook/creating-custom-scroll-directives.html

For Vue 3 users

In vue3 you should use unmounted or beforeUnmount, instead of beforeDestroy.

Lifecycle hook beforeDestroy is not emitted in Vue3

tony19
  • 125,647
  • 18
  • 229
  • 307
agm1984
  • 15,500
  • 6
  • 89
  • 113
  • 3
    Do you really need `lodash` here? Having a `setTimeout` in the `handleScroll` function and clearing the timeout ID if it's not null would do the trick. Anyway, both work but we could avoid the library for 2 lines of code – jeerbl Dec 08 '20 at 17:29
  • My intent was to highlight the difference. It will mostly depend on what exactly is inside your `handleScroll` function and of course how fast the client can execute it. An example of a bad case would be a slow client executing threadblocking code that takes 200ms to execute while `handleScroll` is called every 100ms. – agm1984 Dec 14 '20 at 03:27
  • Note that in vue3 you should use unmounted or beforeUnmount, https://stackoverflow.com/questions/62743811/lifecycle-hook-beforedestroy-is-not-emitted-in-vue3 – Daniel Aug 28 '21 at 17:04
  • @jeerbl actually the latest lodash will only import exactly that debounce function not the entire library. – STREET MONEY Feb 16 '22 at 08:05
  • try `npm install --save lodash.debounce` – agm1984 Feb 16 '22 at 17:40
18

Here's what works directly with Vue custom components.

 <MyCustomComponent nativeOnScroll={this.handleScroll}>

or

<my-component v-on:scroll.native="handleScroll">

and define a method for handleScroll. Simple!

HalfWebDev
  • 7,022
  • 12
  • 65
  • 103
  • 21
    what the hell is `nativeOnScroll`? – Jakub Strebeyko Oct 23 '18 at 10:35
  • 2
    When working with Vue custom components, if you want initiate any native event say - click, scroll, change- you need to use native with it. onChange or onScroll just won't work. As a simple example imagine a `` but `` – HalfWebDev Oct 23 '18 at 12:59
  • Yes, `native` event modifier is a must sometimes. I was under impression that `nativeOnScroll` that gets mentioned is something more that just a custom event emitted by child. Sorry – Jakub Strebeyko Oct 25 '18 at 07:27
  • 1
    Thanks so much ! That solution worked way better for me than defining an event listener ! – LeKevoid Jan 17 '19 at 20:55
  • @kushalvm Could you add full example for `nativeOnScroll`? I assume it is something like `
    ...
    ` inside `MyCustomComponent`. In your answer it gives an impression that it is some built-in Vue event.
    – exmaxx Jul 09 '20 at 09:50
  • this `v-on:scroll.native` is now depcrecated and triggers the error message `error '.native' modifier on 'v-on' directive is deprecated vue/no-deprecated-v-on-native-modifier` – Alexis.Rolland Jan 25 '23 at 06:15
9

I've been in the need for this feature many times, therefore I've extracted it into a mixin. It can be used like this:

import windowScrollPosition from 'path/to/mixin.js'

new Vue({
  mixins: [ windowScrollPosition('position') ]
})

This creates a reactive position property (can be named whatever we like) on the Vue instance. The property contains the window scroll position as an [x,y] array.

Feel free to play around with this CodeSandbox demo.

Here's the code of the mixin. It's thoroughly commentated, so it should not be too hard to get an idea how it works:

function windowScrollPosition(propertyName) {
  return {
    data() {
      return {
        // Initialize scroll position at [0, 0]
        [propertyName]: [0, 0]
      }
    },
    created() {
      // Only execute this code on the client side, server sticks to [0, 0]
      if (!this.$isServer) {
        this._scrollListener = () => {
          // window.pageX/YOffset is equivalent to window.scrollX/Y, but works in IE
          // We round values because high-DPI devies can provide some really nasty subpixel values
          this[propertyName] = [
            Math.round(window.pageXOffset),
            Math.round(window.pageYOffset)
          ]
        }

        // Call listener once to detect initial position
        this._scrollListener()

        // When scrolling, update the position
        window.addEventListener('scroll', this._scrollListener)
      }
    },
    beforeDestroy() {
      // Detach the listener when the component is gone
      window.removeEventListener('scroll', this._scrollListener)
    }
  }
}
tony19
  • 125,647
  • 18
  • 229
  • 307
Loilo
  • 13,466
  • 8
  • 37
  • 47
  • Combine this answer with the debounce (found below) and you have a solid solution! Thanks – Visualize Jul 30 '19 at 13:39
  • 1
    @Visualize This depends a lot on what you want to do on scroll. As long as you don't perform very costly tasks, you actually don't even need a debounce — tested on a 3 year old middle-to-lower-end Android phone (Honor 5C) with this demo: https://vue-scroll-fuicczvgsv.now.sh/ – Loilo Jul 30 '19 at 16:03
7

I know this is an old question, but I found a better solution with Vue.js 2.0+ Custom Directives: I needed to bind the scroll event too, then I implemented this.

First of, using @vue/cli, add the custom directive to src/main.js (before the Vue.js instance) or wherever you initiate it:

Vue.directive('scroll', {
  inserted: function(el, binding) {
    let f = function(evt) {
      if (binding.value(evt, el)) {
        window.removeEventListener('scroll', f);
      }
    }
    window.addEventListener('scroll', f);
  }
});

Then, add the custom v-scroll directive to the element and/or the component you want to bind on. Of course you have to insert a dedicated method: I used handleScroll in my example.

<my-component v-scroll="handleScroll"></my-component>

Last, add your method to the component.

methods: {
  handleScroll: function() {
    // your logic here
  }
}

You don’t have to care about the Vue.js lifecycle anymore here, because the custom directive itself does.

tony19
  • 125,647
  • 18
  • 229
  • 307
Federico Moretti
  • 527
  • 1
  • 11
  • 22
6

I think the best approach is just add ".passive"

v-on:scroll.passive='handleScroll'
N3R4ZZuRR0
  • 2,400
  • 4
  • 18
  • 32
  • While valid for the component itself it will only detect scrolling _inside_ the component. If you want to detect **the whole page scroll** you need to use `window.addEventListener`. See other answers. – exmaxx Jul 21 '20 at 07:00
4

What about something like this? This is Vue 3 by the way

setup() {
    function doOnScroll(event) {
      window.removeEventListener("scroll", doOnScroll);
      console.log("stop listening");
      // your code here ....
      setTimeout(() => {
        window.addEventListener("scroll", doOnScroll, { passive: true });
        console.log("start listening");
      }, 500);
    }

    window.addEventListener("scroll", doOnScroll, { passive: true });
  }

The idea here is to listen for the scroll event only once, do your script and then reattach the scroll listener again with a delay in the setTimeout function. If after this delay the page is still scrolling the scroll event will be handled again. Basically the scroll event is listened only once every 500ms (in this example).

I'm using this just to add a css class during the scroll to move away a button.

mastodilu
  • 119
  • 1
  • 9
2

this does not refresh your component I solved the problem by using Vux create a module for vuex "page"

export const state = {
    currentScrollY: 0,
};

export const getters = {
    currentScrollY: s => s.currentScrollY
};

export const actions = {
    setCurrentScrollY ({ commit }, y) {
        commit('setCurrentScrollY', {y});
    },
};

export const mutations = {
    setCurrentScrollY (s, {y}) {
       s.currentScrollY = y;
    },
};

export default {
    state,
    getters,
    actions,
    mutations,
};

in App.vue :

created() {
    window.addEventListener("scroll", this.handleScroll);
  },
  destroyed() {
    window.removeEventListener("scroll", this.handleScroll);
  },
  methods: {
    handleScroll () {
      this.$store.dispatch("page/setCurrentScrollY", window.scrollY);
      
    }
  },

in your component :

  computed: {
    currentScrollY() {
      return this.$store.getters["page/currentScrollY"];
    }
  },

  watch: {
    currentScrollY(val) {
      if (val > 100) {
        this.isVisibleStickyMenu = true;
      } else {
        this.isVisibleStickyMenu = false;
      }
    }
  },

and it works great.

Farshid
  • 161
  • 1
  • 8
1

Vue3, I added the listener on beforeMount, and it works, just if your case like mine, I need the listener to be triggered on the entire app

beforeMount() {
    window.addEventListener('scroll', this.handleScroll)
},
methods: {
   handleScroll(){
     // work here
  }
}
Majali
  • 480
  • 7
  • 11
1

Vue3 with <script setup> working example:

<template>
  <header :class="stickyHeader ? 'sticky' : ''" ></header>
<template>

<script setup>
import { ref, onBeforeMount } from 'vue'

onBeforeMount(() => {
  window.addEventListener('scroll', handleScroll)
})

const stickyHeader = ref(false)

function handleScroll(){
  if (window.pageYOffset) {
    stickyHeader.value = true
  } else {
    stickyHeader.value = false
  }
}

</script>
Ali Seivani
  • 496
  • 3
  • 21
1

In vue scrolling not a window element. MB in u app will work smth like this:

mounted () {
    document.querySelector('.className').addEventListener('scroll', this.handleScroll);
},

methods: {
    handleScroll(e){
        console.log(this.scrollEl.scrollTop)
    }
},
GrimCap
  • 31
  • 5
-1
document.addEventListener('scroll', function (event) {
    if ((<HTMLInputElement>event.target).id === 'latest-div') { // or any other filtering condition
  
    }
}, true /*Capture event*/);

You can use this to capture an event and and here "latest-div" is the id name so u can capture all scroller action here based on the id you can do the action as well inside here.

Tyler2P
  • 2,324
  • 26
  • 22
  • 31
karan
  • 1
  • 2
-1

In combination this.$vuetify.breakpoint.name with and loading on demand, the scroll event is a really useful feature.

Use a trigger. For example, a tab:

<v-tabs
  v-bind:grow="!isFullScreen()"
  v-bind:vertical="isFullScreen()"
>

Some class attributes:

private isUserScrolling: boolean = false;
private isLoaded: boolean = false;
private startScroll: number = 3;

Function that reacts to the trigger (adjustment if necessary):

private isFullScreen(): boolean {
    switch (this.$vuetify.breakpoint.name) {
      case "xs":
        this.startScroll = 500;
        return false;
      case "sm":
        this.startScroll = 300;
        return false;
      case "md":
        this.startScroll = 100;
        return true;
      case "lg":
        this.startScroll = 50;
        return true;
      case "xl":
        this.startScroll = 3;
        return true;
    }
}

Add your event:

created() {
   window.addEventListener("scroll", this.handleScroll);
}

React to your event:

private handleScroll(event: any): void {
   this.isUserScrolling = window.scrollY > this.startScroll;
   
   if (this.isUserScrolling && !this.isLoaded) {
     // load your stuff
     ...
     this.isLoaded = true;
   }
}
k0nditor
  • 31
  • 5