Kliping
KlipingBerandaDokumentasiKomponenBlocksVariasiTemplateDice UIHooks
Memulai
  • Pengenalan
  • Instalasi
  • Kredit
State
  • useBoolean
  • useControllableValue
  • useCounter
  • useDebounce
  • useResetState
  • useThrottle
  • useToggle
Lanjutan
  • useCreation
  • useCustomCompareEffect
  • useLatest
  • useLockFn
  • useMemoizedFn
  • usePrevious
Siklus Hidup
  • useDebounceEffect
  • useDebounceFn
  • useDeepCompareEffect
  • useDeepCompareLayoutEffect
  • useEffectEvent
  • useEffectWithTarget
  • useInterval
  • useIsHydrated
  • useIsomorphicLayoutEffect
  • useMount
  • useThrottleEffect
  • useThrottleFn
  • useTimeout
  • useUnmount
  • useUpdate
  • useUpdateEffect
Peramban
  • useActiveElement
  • useBattery
  • useClickAnyWhere
  • useClickAway
  • useClipboard
  • useDocumentVisibility
  • useElementSize
  • useEventListener
  • useFps
  • useFullscreen
  • useHash
  • useHover
  • useInViewport
  • useIsMatchMedia
  • useIsOnline
  • useMouse
  • useNetwork
  • useOs
  • useScrollLock
  • useTextSelection
  • useTitle
Pengembangan
  • useWhyDidYouUpdate
Paket Luar
  • useMcp
  • useQuery
  • useStickToBottom
  • useSwr
Pustaka
  • isBrowser
  • createContext
  • bacaElemen
  • saatTerpasang

useStickToBottom

SebelumnyaBerikutnya

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

Halo!

Ini jendela obrolan.

Tambah pesan lalu lihat sendiri.

Menempel: true · di dasar: true. Gulung ke atas lalu tambah pesan — pandangannya tidak ditarik paksa.

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.

Pemasangan ​

npx 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 { bacaElemen } from '@/lib/target'
import { useEventListener } from '@/composables/use-event-listener'
import type { TargetElemen } from '@/lib/target'

export interface UseStickToBottomOptions {
  /**
   * Berapa piksel dari dasar yang masih dihitung "di dasar".
   * @default 24
   */
  ambang?: number
  /**
   * Perilaku gulungan saat menempel otomatis.
   * @default 'smooth'
   */
  behavior?: ScrollBehavior
}

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

  const isAtBottom = ref(true)
  /** Apakah pandangannya sedang dikunci ke dasar. Padam saat orangnya menggulung ke atas. */
  const menempel = ref(true)

  let pengamat: ResizeObserver | undefined
  let pengamatIsi: MutationObserver | undefined

  const wadah = () => bacaElemen(target) as HTMLElement | undefined

  const hitungPosisi = () => {
    const el = wadah()
    if (!el)
      return
    const jarak = el.scrollHeight - el.scrollTop - el.clientHeight
    isAtBottom.value = jarak <= ambang
  }

  const scrollToBottom = (paksa: ScrollBehavior = behavior) => {
    const el = wadah()
    if (!el)
      return
    el.scrollTo({ top: el.scrollHeight, behavior: paksa })
    menempel.value = true
    isAtBottom.value = true
  }

  // Gulungan yang dipicu `scrollTo` sendiri juga membangkitkan peristiwa
  // `scroll`. Tanpa penanda ini, gulungan otomatis akan terbaca sebagai
  // gulungan manual dan langsung melepas penempelannya sendiri.
  let sedangMenggulungSendiri = false

  useEventListener('scroll', () => {
    hitungPosisi()
    if (!sedangMenggulungSendiri)
      menempel.value = isAtBottom.value
  }, { target, passive: true })

  const ikutiDasar = () => {
    if (!menempel.value)
      return
    sedangMenggulungSendiri = true
    scrollToBottom()
    // Dilepas di tugas berikutnya, setelah peristiwa scroll-nya lewat.
    setTimeout(() => {
      sedangMenggulungSendiri = false
    }, 0)
  }

  const bongkar = () => {
    pengamat?.disconnect()
    pengamatIsi?.disconnect()
    pengamat = undefined
    pengamatIsi = undefined
  }

  const berhentiPantau = watch(
    () => wadah(),
    (el) => {
      bongkar()
      if (!el)
        return

      hitungPosisi()

      // Dua pengamat, karena isinya bisa bertambah dengan dua cara: elemen baru
      // disisipkan, atau elemen yang sudah ada berubah tinggi — gambar yang
      // selesai dimuat, teks yang membungkus ulang.
      if (typeof ResizeObserver !== 'undefined') {
        pengamat = new ResizeObserver(ikutiDasar)
        pengamat.observe(el)
        for (const anak of Array.from(el.children))
          pengamat.observe(anak)
      }

      if (typeof MutationObserver !== 'undefined') {
        pengamatIsi = new MutationObserver(() => {
          if (pengamat) {
            for (const anak of Array.from(el.children))
              pengamat.observe(anak)
          }
          ikutiDasar()
        })
        pengamatIsi.observe(el, { childList: true, subtree: true, characterData: true })
      }
    },
    { immediate: true, flush: 'post' },
  )

  const berhenti = () => {
    bongkar()
    berhentiPantau()
  }

  if (getCurrentScope())
    onScopeDispose(berhenti)

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

API ​

ts
export interface UseStickToBottomOptions {
  /**
   * Berapa piksel dari dasar yang masih dihitung "di dasar".
   * @default 24
   */
  ambang?: number
  /**
   * Perilaku gulungan saat menempel otomatis.
   * @default 'smooth'
   */
  behavior?: ScrollBehavior
}

export function useStickToBottom(
  target: TargetElemen,
  options: UseStickToBottomOptions = {},
)
useQueryuseSwr

Di halaman ini

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