Продолжение темы умный sticky в Vue

от автора

в
Время чтения: 2 мин.

В первом варианте мы реализовали “умный sticky” через отслеживание скролла и накопление смещения (delta).

Это даёт полный контроль, но у такого подхода есть особенность:

положение сайдбара зависит не только от текущего scroll, но и от его истории

Иногда это нормально, но можно сделать проще:

👉 не хранить промежуточное состояние вообще
👉 а вычислять позицию напрямую из текущего scroll


💡 Идея

Вместо:

  • “прибавь delta к текущему offset”

мы делаем:

“в любой момент времени посчитай offset заново”

То есть:

  • без watch
  • без lastScrollY
  • без накопления ошибок

🧩 Код компонента

<script setup lang="ts">
import { computed, ref } from 'vue'
import { useElementBounding, useWindowScroll, useWindowSize } from '@vueuse/core'

const props = withDefaults(defineProps<{
  offsetTop?: number
  offsetBottom?: number
}>(), {
  offsetTop: 0,
  offsetBottom: 0,
})

const containerRef = ref<HTMLElement | null>(null)
const sidebarRef = ref<HTMLElement | null>(null)

const { y: scrollY } = useWindowScroll()
const { height: windowHeight } = useWindowSize()

const { top: containerTopAbs } = useElementBounding(containerRef)
const { height: sidebarHeight } = useElementBounding(sidebarRef)

const availableHeight = computed(() => {
  return windowHeight.value - props.offsetTop - props.offsetBottom
})

const overflow = computed(() => {
  return Math.max(0, sidebarHeight.value - availableHeight.value)
})

const startScroll = computed(() => {
  return containerTopAbs.value - props.offsetTop
})

const offsetY = computed(() => {
  if (overflow.value <= 0) {
    return 0
  }

  const rawOffset = scrollY.value - startScroll.value
  return Math.max(0, Math.min(rawOffset, overflow.value))
})
</script>

<template>
  <div ref="containerRef" class="relative">
    <div
      ref="sidebarRef"
      class="sticky"
      :style="{
        top: `${props.offsetTop - offsetY}px`,
      }"
    >
      <slot />
    </div>
  </div>
</template>

🔍 В чём отличие от первого варианта

Визуально поведение почти такое же:

  • сайдбар прилипает
  • если длинный — начинает “прокручиваться внутри”
  • не выходит за границы

Но логика внутри сильно проще.


📐 Как считается смещение

Ключевая часть:

const rawOffset = scrollY.value - startScroll.value

👉 это ответ на вопрос:

“насколько пользователь уже проскроллил относительно точки прилипания”


Дальше просто ограничение:

offset = clamp(rawOffset, 0, overflow)

Где:

  • 0 — ещё не начали двигаться
  • overflow — максимум, на который можно сдвинуть

📏 Что такое overflow

overflow = высота сайдбара - доступная высота окна

Если:

  • overflow <= 0 → всё помещается
  • overflow > 0 → нужно смещать

📍 Стартовая точка

startScroll = containerTop - offsetTop

👉 момент, когда sticky “включается”

До этого:

  • offset = 0
  • сайдбар ведёт себя как обычный

🎯 Само смещение

В шаблоне:

top: offsetTop - offsetY

👉 это тот же приём, что и в первой статье:

  • уменьшаем top
  • тем самым двигаем контент вверх

🧠 Почему это может быть удобнее

Этот вариант:

  • не зависит от предыдущего состояния
  • легче дебажить
  • проще переносится
  • устойчивее к резким изменениям layout

Фактически это:

derived state вместо накопительного


⚖️ Когда какой вариант выбрать

Первый вариант (через delta)

Подойдёт, если:

  • нужна тонкая реакция на направление скролла
  • важен “плавный контроль”
  • есть сложная логика поведения

Этот вариант (через вычисление)

Подойдёт, если:

  • хочется проще
  • поведение линейное
  • нет необходимости учитывать направление скролла

🧾 Итог

Оба подхода решают одну и ту же задачу, но делают это по-разному:

  • первый — через накопление состояния
  • второй — через вычисление состояния

И часто второй вариант оказывается:
👉 проще и устойчивее, чем кажется на первый взгляд


В следующей статье рассмотрим другой способ.
👉 полный контроль через position: fixed и переключение состояний


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Сколько будет 5 + 5?