Element with Autoscroll in Vue.js

Autoscroll is important to handle dynamic content such as chats, logs, and real-time data feeds.

Basic element with scroll

First of all we create simple element with scroll and button to append new lines:

<template>
<div style="border: 1px solid gray; padding: .5em;">
  <div style="overflow: auto; max-height: 100px;">
    <div v-for="i in list" :key="i">{{ i }}</div>
  </div>
</div>

<button @click="list.push(list.length + 1)">Append line</button>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const list = ref(Array.from({ length: 10 }, (_, index) => index + 1))
</script>

First div element is just a container with border. Second div element has two key options: scroll on overflow, and a maximum height limit to achieve this overflow.

Scroll to bottom when element ready

<template>
<div style="border: 1px solid gray; padding: .5em;">
  <div ref="el" style="overflow: auto; max-height: 100px;">
    <div v-for="i in list" :key="i">{{ i }}</div>
  </div>
</div>

<button @click="list.push(list.length + 1)">Append line</button>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'

const list = ref(Array.from({ length: 10 }, (_, index) => index + 1))
const el = ref<HTMLElement | null>(null)

onMounted(() => {
  el.value?.scrollTo({
    top: el.value.scrollHeight,
  })
})
</script>

Variable el is a reference to the element. When the component is mounted to the DOM, this element will be scrolled down.

VueUse

For better productivity I recommend to use VueUse library. Install it with npm:

npm i @vueuse/core

Autoscroll with MutationObserver

MutationObserver is an interface provides the ability to watch for changes being made to the DOM.

<template>
<div style="border: 1px solid gray; padding: .5em;">
  <div ref="el" style="overflow: auto; max-height: 100px;">
    <div v-for="i in list" :key="i">{{ i }}</div>
  </div>
</div>

<button @click="list.push(list.length + 1)">Append line</button>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useMutationObserver } from '@vueuse/core'

const list = ref(Array.from({ length: 10 }, (_, index) => index + 1))
const el = ref<HTMLElement | null>(null)

onMounted(() => {
  el.value?.scrollTo({
    top: el.value.scrollHeight,
  })
})

useMutationObserver(el, () => {
  el.value?.scrollTo({
    behavior: 'smooth',
    top: el.value.scrollHeight,
  })
}, {
    childList: true
})
</script>

MutationObserver starts callback when child elements added.

Turn off autoscroll

Annoying situation when you scroll up to find something in the list and autoscroll bring you back to the bottom. Let's turn autoscroll off when it not needed:

<template>
<div style="border: 1px solid gray; padding: .5em;">
  <div
    ref="el"
    @scroll="setAutoscroll"
    style="overflow: auto; max-height: 100px;"
  >
    <div v-for="i in list" :key="i">{{ i }}</div>
  </div>
</div>

<button @click="list.push(list.length + 1)">Append line</button>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useMutationObserver } from '@vueuse/core'

const list = ref(Array.from({ length: 10 }, (_, index) => index + 1))
const el = ref<HTMLElement | null>(null)

const autoscroll = ref(true)
const setAutoscroll = (ev: Event) => {
  const target = ev.target as HTMLElement
  const threshold = 10
  autoscroll.value = threshold + target.scrollTop + target.clientHeight >= target.scrollHeight
}

onMounted(() => {
  el.value?.scrollTo({
    top: el.value.scrollHeight,
  })
})

useMutationObserver(el, () => {
  if(autoscroll.value) {
    el.value?.scrollTo({
      behavior: 'smooth',
      top: el.value.scrollHeight,
    })
  }
}, {
    childList: true
})
</script>