position: sticky — одна из тех вещей, которые кажутся идеальными…
пока не начинаешь использовать их в реальном интерфейсе.
Особенно если у тебя:
- длинный сайдбар
- фильтры
- или просто блок, который выше экрана
В какой-то момент всё начинает ломаться.
В этой статье разберём решение:
👉 sticky-блок с “внутренней прокруткой” без overflow: auto
❌ Проблема обычного sticky
Классический вариант:
position: sticky;
top: 0;
Работает отлично, пока:
- контент меньше высоты viewport
Но если больше — начинаются проблемы:
- часть контента становится недоступной
- появляется вложенный скролл
- UX превращается в боль
💡 Идея решения
Вместо того чтобы:
добавлять
overflow: autoи плодить скроллы
мы делаем иначе:
двигаем сам sticky-блок внутри себя
То есть:
- sticky остаётся sticky
- но его содержимое “прокручивается” за счёт смещения
🧩 Полный код компонента
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { useWindowScroll, useElementBounding } from '@vueuse/core'
const props = defineProps<{
offsetTop?: number
offsetBottom?: number
}>()
const containerRef = ref<HTMLElement | null>(null)
const stickyRef = ref<HTMLElement | null>(null)
const { y: scrollY } = useWindowScroll()
const lastScrollY = ref(0)
const offsetY = ref(0)
const { height: stickyHeight } = useElementBounding(stickyRef)
const { top: containerTop } = useElementBounding(containerRef)
const topOffset = computed(() => props.offsetTop ?? 0)
const bottomOffset = computed(() => props.offsetBottom ?? 0)
const clamp = (val: number, min: number, max: number) => Math.max(min, Math.min(max, val))
watch(scrollY, (newY) => {
const delta = newY - lastScrollY.value
lastScrollY.value = newY
const stickyH = stickyHeight.value
const containerT = containerTop.value
const available = window.innerHeight - topOffset.value - bottomOffset.value
const overflow = stickyH - available
if (overflow <= 0 || containerT > topOffset.value) {
offsetY.value = 0
return
}
offsetY.value = clamp(offsetY.value + delta, 0, overflow)
})
</script>
<template>
<div ref="containerRef" class="relative">
<div
ref="stickyRef"
class="sticky"
:style="{
top: `-${offsetY}px`,
}"
>
<slot />
</div>
</div>
</template>🔍 Как это работает
Разберём ключевые части.
1. Отслеживание скролла
Компонент слушает позицию страницы:
// scrollY
И вычисляет разницу:
// delta = текущий скролл - предыдущий
👉 Это даёт понимание:
- пользователь скроллит вниз или вверх
2. Определение доступного пространства
Ключевая формула:
// available = высота окна - отступы
// overflow = высота sticky - доступное пространство
👉 Здесь происходит магия:
- если
overflow <= 0→ всё помещается - если
overflow > 0→ нужно “скроллить внутри”
3. Когда ничего делать не нужно
// если элемент помещается
// или ещё не дошёл до sticky-зоны
👉 тогда:
- offset сбрасывается
- поведение как у обычного sticky
4. Основная логика движения
// offset += delta
// ограничиваем через clamp
👉 мы:
- накапливаем смещение
- но не даём выйти за пределы
5. Ограничение (clamp)
// clamp(offset, 0, overflow)
Это важно:
- не даёт улететь вверх
- не даёт выйти за нижнюю границу
6. Сам трюк
В шаблоне:
// top: -offsetY
👉 вместо обычного:
top: 0;
мы делаем:
topотрицательным- и двигаем контент вверх
🎯 Что получается в итоге
Поведение выглядит так:
- Скроллим страницу
- Блок “прилипает”
- Когда контент заканчивает помещаться:
- он начинает “прокручиваться внутри”
- Дошёл до конца → зафиксировался
И всё это:
👉 без второго скролла
🧪 Где это реально полезно
- сайдбар фильтров (e-commerce)
- оглавление статьи
- админ-панели
- панели инструментов
- любые “длинные” sticky-блоки
⚠️ Подводные камни
Частые обновления при скролле
Каждое движение вызывает пересчёт:
// watch(scrollY)
👉 решение:
- throttle / debounce
Зависимость от окна
// window.innerHeight
👉 может быть проблемой при SSR
Динамический контент
Если внутри:
- раскрываются блоки
- подгружается контент
👉 высота меняется → расчёты могут поехать
🚀 Как можно улучшить
Если хочется довести до продакшн-идеала:
- добавить
useThrottleFn - реагировать на resize
- использовать
transformвместоtop(плавнее) - вынести в composable
- добавить поддержку анимации
🧠 Итог
Этот компонент — хороший пример того, как:
не бороться с браузером, а использовать его механику
Ты не создаёшь новый скролл.
Ты просто управляешь поведением sticky.
В итоге получаешь:
- чистый UX
- один скролл
- предсказуемое поведение
Если делаешь админки, маркетплейсы или просто сложные интерфейсы —
это один из тех паттернов, который реально стоит иметь в арсенале.


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