В первом варианте мы реализовали “умный 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 и переключение состояний


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