kliping.pro
  • Docs
  • Directory
  • Colors
  • Typeset
Hooks
  • State7
  • Advanced6
  • Lifecycle16
  • Browser21
  • Development1
  • Third-Party4
    • useMcp
    • useQuery
    • useStickToBottom
    • useSwr
  • Libraries4

useStickToBottom

PreviousNext

Menahan sebuah wadah gulung tetap menempel di dasarnya saat isinya bertambah.

Ini yang membuat jendela obrolan terasa benar: pesan baru muncul dan pandangannya ikut turun — kecuali kalau orangnya sedang menggulung ke atas membaca yang lama. Menariknya kembali ke bawah saat itu adalah salah satu hal paling menjengkelkan yang bisa dilakukan sebuah antarmuka.

Tidak ada padanan Vue untuk paket use-stick-to-bottom yang dipakai sumbernya, jadi ini ditulis sendiri. Bahannya sudah ada di repo ini — src/components/ui/message-scroller/ melakukan hal yang sama di dalam komponen; yang dikerjakan di sini adalah mengangkat logikanya keluar supaya bisa dipakai wadah gulung apa pun.

Ambang toleransinya ada karena scrollTop pecahan. Peramban melaporkan angka seperti 1247.3999 untuk posisi yang secara visual sudah mentok, dan perbandingan persis tidak akan pernah benar.

Installation ​

pnpm dlx shadcn-vue@latest add @kliping/use-stick-to-bottom

Perlu @kliping terdaftar di components.json project Anda. Lihat cara menyiapkannya.

Ikut terpasang sendiri: target, use-event-listener.

Salin dulu target, is-browser, use-event-listener — masing-masing memuat sumbernya di halamannya sendiri — lalu tempel berkas ini.

ts
import { getCurrentScope, onScopeDispose, readonly, ref, watch } from 'vue'
import { getTargetElement } from '@/lib/target'
import { useEventListener } from '@/composables/use-event-listener'
import type { BasicTarget } from '@/lib/target'

export interface UseStickToBottomOptions {
  /** Distance in pixels from the bottom that still counts as "at the bottom". Default: 24. */
  threshold?: number
  /** Scroll behavior used when auto-sticking. Default: `'smooth'`. */
  behavior?: ScrollBehavior
}

export function useStickToBottom(target: BasicTarget, options: UseStickToBottomOptions = {}) {
  const { threshold = 24, behavior = 'smooth' } = options

  const isAtBottom = ref(true)
  /** Whether the view is pinned to the bottom; turns off when the user scrolls up. */
  const sticky = ref(true)

  let resizeObserver: ResizeObserver | undefined
  let mutationObserver: MutationObserver | undefined

  const container = () => getTargetElement(target) as HTMLElement | undefined

  const updatePosition = () => {
    const el = container()
    if (!el) return
    const distance = el.scrollHeight - el.scrollTop - el.clientHeight
    isAtBottom.value = distance <= threshold
  }

  const scrollToBottom = (scrollBehavior: ScrollBehavior = behavior) => {
    const el = container()
    if (!el) return
    el.scrollTo({ top: el.scrollHeight, behavior: scrollBehavior })
    sticky.value = true
    isAtBottom.value = true
  }

  let isAutoScrolling = false

  useEventListener(
    'scroll',
    () => {
      updatePosition()
      if (!isAutoScrolling) sticky.value = isAtBottom.value
    },
    { target, passive: true },
  )

  const followBottom = () => {
    if (!sticky.value) return
    isAutoScrolling = true
    scrollToBottom()
    setTimeout(() => {
      isAutoScrolling = false
    }, 0)
  }

  const teardown = () => {
    resizeObserver?.disconnect()
    mutationObserver?.disconnect()
    resizeObserver = undefined
    mutationObserver = undefined
  }

  const stopWatch = watch(
    () => container(),
    (el) => {
      teardown()
      if (!el) return

      updatePosition()

      if (typeof ResizeObserver !== 'undefined') {
        resizeObserver = new ResizeObserver(followBottom)
        resizeObserver.observe(el)
        for (const child of Array.from(el.children)) resizeObserver.observe(child)
      }

      if (typeof MutationObserver !== 'undefined') {
        mutationObserver = new MutationObserver(() => {
          if (resizeObserver) {
            for (const child of Array.from(el.children)) resizeObserver.observe(child)
          }
          followBottom()
        })
        mutationObserver.observe(el, { childList: true, subtree: true, characterData: true })
      }
    },
    { immediate: true, flush: 'post' },
  )

  const stop = () => {
    teardown()
    stopWatch()
  }

  if (getCurrentScope()) onScopeDispose(stop)

  return {
    isAtBottom: readonly(isAtBottom),
    isSticky: readonly(sticky),
    scrollToBottom,
    stop: stop,
  }
}

API ​

ts
export interface UseStickToBottomOptions {
  /** Distance in pixels from the bottom that still counts as "at the bottom". Default: 24. */
  threshold?: number
  /** Scroll behavior used when auto-sticking. Default: `'smooth'`. */
  behavior?: ScrollBehavior
}

export function useStickToBottom(
  target: BasicTarget,
  options: UseStickToBottomOptions = {},
)
PrevioususeQueryNext useSwr

On This Page

InstallationAPI
Dirilis di bawah lisensi MIT. Ditulis ulang untuk Vue dari shadcn-vue, shadcn-ui-blocks, dan shadcn-hooks. Hak cipta © 2026 Kliping