103

I have a Vue.js component with several elements in it. I want to automatically scroll to the bottom of that element when a method in the component is called.

Basically, do the same as this. However, I haven't found a way to get the element within my component and modify scrollTop

I'm currently using Vue.js 2.0.8.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
angrykoala
  • 3,774
  • 6
  • 30
  • 55
  • worked fine for me http://stackoverflow.com/questions/11715646/scroll-automatically-to-the-bottom-of-the-page – yo yo Feb 22 '17 at 01:28

17 Answers17

132

2023 smooth scrolling ability, easy, readable, and won't hurt your brain... use el.scrollIntoView()

scrollIntoView() has options you can pass it like scrollIntoView({behavior: 'smooth'}) to get smooth scrolling out of the box and does not require any external libraries.

methods: {
  scrollToElement() {
    const el = this.$refs.scrollToMe;

    if (el) {
      // Use el.scrollIntoView() to instantly scroll to the element
      el.scrollIntoView({behavior: 'smooth'});
    }
  }
}

Then if you wanted to scroll to this element on page load you could call this method like this:

mounted() {
  this.scrollToElement();
}

Else if you wanted to scroll to it on a button click or some other action you could call it the same way:

<button @click="scrollToElement">scroll to me</button>

The scroll works all the way down to IE 8. The smooth scroll effect does not work out of the box in IE or Safari. If needed there is a polyfill available for this here as @mostafaznv mentioned in the comments.

Here is a fiddle.

maxshuty
  • 9,708
  • 13
  • 64
  • 77
  • I like this solution, it looks quite simple, however, this only scrolls an element into view, not to the bottom of the div. After checking the docs, using `scrollIntoView(false)` seems to work that way – angrykoala Sep 02 '19 at 11:54
  • 1
    Shouldn't it be instead of . Remeber, @ is shorthand for v-on: and : is short for v-bind: – Rohit Nair Sep 05 '19 at 04:22
  • 1
    This scrolls an element into view, worked perfectly for me. Upvoted – James Shisiah Nov 26 '19 at 17:20
  • 3
    I had to use updated instead monted, but worked perfectly ;) – dippas Jun 03 '20 at 01:05
  • Thanks for the fiddle! I would put the fiddle code here as well. – lenooh Jul 10 '20 at 07:01
  • Safari does not support {behavior: 'smooth'}. – Tong Jul 21 '20 at 23:27
  • Thanks @ERU, I've updated my answer to point that out. – maxshuty Jul 22 '20 at 12:48
  • 1
    thanks for solution. you can use this polyfill https://github.com/stipsan/scroll-into-view-if-needed – mostafaznv Aug 24 '20 at 07:42
  • 1
    Thanks @mostafaznv for finding that - I've updated my answer with your polyfill suggestion. – maxshuty Aug 24 '20 at 14:34
  • how woould i do this for the last element added?? .pop() or .splice(-1)[0] doesn't work in my case. I have a v-for="(chat, index) in obj.foo". have tried const el = this.obj.foo.splice(-1)[0], but that returns a this is not a function. and the more i think about it, it needs to link to the :class, and this doesn't. thanks! – Teoman Kirac Feb 02 '21 at 15:25
  • @TeomanKirac can you just get the parent element then get the last child and do the `scrollIntoView()` on that? – maxshuty Feb 02 '21 at 15:31
  • 1
    Good advice never hurt your brain I searched for more than 2 hrs... thanks works like a charm. – narasimharaosp Oct 04 '21 at 15:07
  • 1
    This is great. I was able to add `mounted() {this.$el.scrollIntoView();}` to a component that gets generated with a `v-for` loop, to automatically scroll to a new element when it's added. – Dustin Michels Oct 07 '21 at 20:51
  • what if I want a horizontal scroll? i tested this codes but it seems like just working in vertical scroll – Choy Jan 23 '22 at 04:43
  • @Choy see this answer: https://stackoverflow.com/a/58537189/4826740 – maxshuty Jan 23 '22 at 10:59
119

As I understand, the desired effect you want is to scroll to the end of a list (or scrollable div) when something happens (e.g.: an item is added to the list). If so, you can scroll to the end of a container element (or even the page it self) using only pure JavaScript and the VueJS selectors.

var container = this.$el.querySelector("#container");
container.scrollTop = container.scrollHeight;

I've provided a working example in this fiddle.

Every time a item is added to the list, the list is scrolled to the end to show the new item.

starball
  • 20,030
  • 7
  • 43
  • 238
Italo Ayres
  • 2,603
  • 2
  • 16
  • 20
  • 39
    Instead of `this.$el.querySelector` you can also get your desired div by selecting it with the [ref](https://vuejs.org/v2/api/#ref) mechanism. – Existe Deja Sep 12 '18 at 07:49
  • 3
    If no solution works, maybe you should google $nextTick(). You have to wait that the content is fully drawn before scrolling. (check Alpesh's answer) – maugch Sep 07 '20 at 15:27
  • 1
    **For everyone using NUXT**: You have to put `scrollToTop: false` on your page for it to work between changing routes – tObi Sep 22 '20 at 11:08
  • 1
    @GeorgeAbitbol Refs are more of a last-resort (hack), and reliance on them can lead to problems with dynamic components: e.g., the $refs object is not reactive, so it continues to grow as components are added and deleted. – PeterT Nov 28 '20 at 06:30
  • Why not just use `window.scrollTo(0, document.body.scrollHeight);` ? – nonNumericalFloat Jul 26 '21 at 09:48
  • This worked for me, but I did use `this.$refs` to get the element. – Jesús Vera Jan 27 '22 at 13:36
  • It may not scroll to element precisely if you have a bunch of v-img tags (or anything that loads dynamically) that load when you scroll over them thus adding to window's overall height. – Glenn Carver Nov 26 '22 at 15:42
41

I tried the accepted solution and it didn't work for me. I use the browser debugger and found out the actual height that should be used is the clientHeight BUT you have to put this into the updated() hook for the whole solution to work.

data(){
return {
  conversation: [
    {
    }
  ]
 },
mounted(){
 EventBus.$on('msg-ctr--push-msg-in-conversation', textMsg => {
  this.conversation.push(textMsg)
  // Didn't work doing scroll here
 })
},
updated(){              <=== PUT IT HERE !!
  var elem = this.$el
  elem.scrollTop = elem.clientHeight;
},
juliangonzalez
  • 4,231
  • 2
  • 37
  • 47
  • It is good solution, but any actions with component triggers updated hook, and at every update element will be scrolled bottom – schrodingercat Jul 26 '18 at 14:38
  • 1
    Thank you, I had the same problem with the accepted answer. Your answer solved it for me as well. :) – Subtractive Jul 27 '18 at 12:37
  • 1
    A better way would be to put this inside the relevant watcher method, as a callback of (or having awaited) `$nextTick()`. This will have the same effect, but possibly need significantly less resources. – phil294 Sep 28 '18 at 19:26
  • 2
    **For everyone using NUXT**: You have to put `scrollToTop: false` on your page for it to work between changing routes – tObi Sep 22 '20 at 11:07
16
  1. Use the ref attribute on the DOM element for reference
<div class="content scrollable" ref="msgContainer">
    <!-- content -->
</div>
  1. You need to setup a WATCH
  data() {
    return {
      count: 5
    };
  },
  watch: {
    count: function() {
      this.$nextTick(function() {
        var container = this.$refs.msgContainer;
        container.scrollTop = container.scrollHeight + 120;
      });
    }
  }
  1. Ensure you're using proper CSS
.scrollable {
  overflow: hidden;
  overflow-y: scroll;
  height: calc(100vh - 20px);
}
maxshuty
  • 9,708
  • 13
  • 64
  • 77
Alpesh Patil
  • 1,672
  • 12
  • 15
  • 1
    Very helpful hint. I initially solved it with setTimeout, but this is clearly the right solution to get the timing right reliably. If you don´t use "nextTick" scrolling might happen before rendering – Marian Klühspies May 17 '20 at 10:56
  • 1
    nextTick solved my problem. Without it it didn't work. Thanks Alpesh – LauroSkr Oct 26 '20 at 10:07
  • thanks, most solutions here will work fine on click, but the only way to make it work on mounted is to use "nextTick" – Moauya Meghari Mar 23 '21 at 08:35
9

Here is a simple example using ref to scroll to the bottom of a div.

/*
Defined somewhere:
  var vueContent = new Vue({
      el: '#vue-content',
      ...
 */

var messageDisplay = vueContent.$refs.messageDisplay;
messageDisplay.scrollTop = messageDisplay.scrollHeight;
<div id='vue-content'>
  <div ref='messageDisplay' id='messages'>
    <div v-for="message in messages">
      {{ message }}
    </div>
  </div>
</div>

Notice that by putting ref='messageDisplay' in the HTML, you have access to the element through vueContent.$refs.messageDisplay

maxshuty
  • 9,708
  • 13
  • 64
  • 77
capncanuck
  • 383
  • 1
  • 3
  • 8
8

If you need to support IE11 and (old) Edge, you can use:

scrollToBottom() {
    let element = document.getElementById("yourID");
    element.scrollIntoView(false);
}

If you don't need to support IE11, the following will work (clearer code):

scrollToBottom() {
    let element = document.getElementById("yourID");
    element.scrollIntoView({behavior: "smooth", block: "end"});
}
Katinka Hesselink
  • 3,961
  • 4
  • 20
  • 26
5

Try vue-chat-scroll:

Install via npm: npm install --save vue-chat-scroll

Import:

import Vue from 'vue'
import VueChatScroll from 'vue-chat-scroll'
Vue.use(VueChatScroll)

in app.js after window.Vue = require('vue').default;

then use it with :

<ul class="messages" v-chat-scroll>
  // your message/chat code...
</ul>
Daljit Singh
  • 270
  • 3
  • 10
4

For those that haven't found a working solution above, I believe I have a working one. My specific use case was that I wanted to scroll to the bottom of a specific div - in my case a chatbox - whenever a new message was added to the array.

const container = this.$el.querySelector('#messagesCardContent');
        this.$nextTick(() => {
          // DOM updated
          container.scrollTop = container.scrollHeight;
        });

I have to use nextTick as we need to wait for the dom to update from the data change before doing the scroll!

I just put the above code in a watcher for the messages array, like so:

messages: {
      handler() {
        // this scrolls the messages to the bottom on loading data
        const container = this.$el.querySelector('#messagesCard');
        this.$nextTick(() => {
          // DOM updated
          container.scrollTop = container.scrollHeight;
        });
      },
      deep: true,
    }, 
  • watch: { arrayName: { handler() { const container = this.$el.querySelector("#idName"); this.$nextTick(() => { container.scrollTop = container.scrollHeight; }); }, deep: true, }, }, I find this method useful, thank you – Saleheen Noor Jun 06 '22 at 23:16
3

In the related question you posted, we already have a way to achieve that in plain javascript, so we only need to get the js reference to the dom node we want to scroll.

The ref attribute can be used to declare reference to html elements to make them available in vue's component methods.

Or, if the method in the component is a handler for some UI event, and the target is related to the div you want to scroll in space, you can simply pass in the event object along with your wanted arguments, and do the scroll like scroll(event.target.nextSibling).

tony19
  • 125,647
  • 18
  • 229
  • 307
JJPandari
  • 3,454
  • 1
  • 17
  • 24
  • I found that this options seems to work fine, however, the rendering of the string in the div occurs _after_ setting the scroll (I'm updating the string and the scroll on the same method). Is there any way to change the scroll position after the text is being updated by vue? – angrykoala Nov 23 '16 at 00:30
  • Vue.nextTick(function(){}) waits for the next update. – Igor Skoric Feb 18 '17 at 16:49
3

The solution did not work for me but the following code works for me. I am working on dynamic items with class of message-box.

scrollToEnd() {
            setTimeout(() => {
                this.$el
                    .getElementsByClassName("message-box")
                    [
                        this.$el.getElementsByClassName("message-box").length -
                            1
                    ].scrollIntoView();
            }, 50);
        }

Remember to put the method in mounted() not created() and add class message-box to the dynamic item.

setTimeout() is essential for this to work. You can refer to https://forum.vuejs.org/t/getelementsbyclassname-and-htmlcollection-within-a-watcher/26478 for more information about this.

Gerald H
  • 454
  • 4
  • 13
3

This is what worked for me

this.$nextTick(() => {
   let scrollHeight = this.$refs.messages.scrollHeight
   window.scrollTo(0, scrollHeight)
})
Timothy
  • 91
  • 1
  • 6
2

I had the same need in my app (with complex nested components structure) and I unfortunately did not succeed to make it work.

Finally I used vue-scrollto that works fine !

Ousmane
  • 2,673
  • 3
  • 30
  • 37
2

My solutions without modules:

Template

<div class="scrollable-content" ref="conversations" />

Script

scrollToBottom() {
  const container = this.$refs.conversations;

  container.scrollTop = container.scrollHeight;
},
Quang Dong
  • 467
  • 5
  • 7
1

Agree with Lurein Perera

Just want to add extra info

watch: {
    arrayName: {
      handler() {
        const container = this.$el.querySelector("#idName");
        this.$nextTick(() => {
          container.scrollTop = container.scrollHeight;
        });
      },
      deep: true,
    },
  },

Where as:

arrayName = Name of array

idName = The id attribute has to be added to the div where you want the scrollbar to auto-scroll down when arrayName length increases.

Saleheen Noor
  • 261
  • 2
  • 8
1

Using Composition API and TypeScript


I set the parameter scrollTop equal to scrollHeightfrom the HTMLDivElment API.

    <template>
     <div id="container" ref="comments">
      Content ...
     </div>
    </template>
    <script lang="ts">
    import { defineComponent, ref, Ref, watchEffect } from 'vue'

    export default defineComponent({
     setup() {
      const comments: Ref<null | HTMLDivElement> = ref(null)

      watchEffect(() => {
       if(comments.value) {
        comments.value.scrollTop = comments.value.scrollHeight
       }
      })

      return {
       comments
      }
     }
    })
    </script>
eXception
  • 1,307
  • 1
  • 7
  • 22
Hugo Ramirez
  • 448
  • 4
  • 6
0
scrollToBottom() {
    this.$nextTick(function () {
        let BoxEl = document.querySelector('#Box');
        if(BoxEl)
            BoxEl.scrollTop = BoxEl.scrollHeight;
    });
}
Duncanmoo
  • 3,535
  • 2
  • 31
  • 32
0
  scrollToElement() {
    const element = this.$refs.abc; // here abc is the ref of the element

    if (element) {
      el.scrollIntoView({behavior: 'smooth'});
    }
  }
}

here you need to use ref for the particular div or element which you want make visible on scroll.

if you have a table and you want to locate the last row of the table then you have to use -

element.lastElementChild.scrollIntoView({behaviour:'smooth'})

Here not that if you ware asynchronously adding the element to the table then you have to take care of it. you can test it using setTimeout, if that is making any difference.

e.g.

    const element = this.$refs.abc;
    if (element) {
      setTimeout(() => {
        element.lastElementChild.scrollIntoView({behaviour:'smooth'})
      }, 1000);

    }
  }

replace set timeout with your own async logic.

Sanket Sardesai
  • 399
  • 4
  • 6