Прокрутка и scrollIntoView
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Прокрутка — свойство не страницы, а любого блока, которому тесно внутри своих границ. Браузер даёт три уровня доступа: прочитать позицию числом, задать её самому и попросить scrollIntoView посчитать всё за тебя.
У каждого прокручиваемого элемента есть scrollTop — сколько пикселей содержимого уехало вверх за верхнюю границу. Ноль — самый верх, максимум примерно равен scrollHeight - clientHeight: это те самые размеры из статьи «Размеры элемента: offset, client, scroll», где остаток до низа уже считали. Свойство не только читается, но и записывается: присвоил число — блок мгновенно прыгнул. Больше максимума присвоить нельзя, браузер молча обрежет.
Для страницы целиком ту же роль играет window.scrollY (и window.scrollX по горизонтали). Это ровно то же число, что document.documentElement.scrollTop, только доступное лишь на чтение. Старое имя pageYOffset — полный синоним, его ещё встретишь в чужом коде.
Присваивать scrollTop руками приходится редко: есть scrollTo — прокрутить к указанной позиции, и scrollBy — сдвинуть относительно текущей. Оба метода есть и у элементов, и у window: window.scrollTo(0, 0) — классическая кнопка «наверх».
У каждого две формы вызова. Короткая берёт два числа в порядке x, y — порядок легко перепутать, потому что читаем мы обычно «сверху вниз». Объектная форма { top, left, behavior } длиннее, зато самоописательна, а left в ней можно не указывать.
У объектной формы есть behavior: "instant" — мгновенно, "smooth" — с анимацией, "auto" (по умолчанию) — «как решит CSS». Именно поэтому свойство scroll-behavior: smooth на контейнере делает плавным даже обычное присваивание scrollTop: оно тоже считается прокруткой в режиме auto. А явный "instant" сильнее CSS всегда.
Главная ловушка плавности — асинхронность. Сразу после вызова с "smooth" в scrollTop лежит ещё старое значение: анимация идёт по кадрам. Если нужно что-то сделать по прибытии, слушай событие scrollend, а не читай позицию следующей строкой.
Плавность нравится не всем. У части людей в системе включено prefers-reduced-motion, и длинная анимация прокрутки для них не красота, а укачивание. Проверяется одной строкой: window.matchMedia("(prefers-reduced-motion: reduce)").matches — и behavior становится instant.
Для прокруток в режиме auto ту же просьбу учитывают и без скрипта: scroll-behavior: smooth кладут внутрь @media (prefers-reduced-motion: no-preference) { … }, и плавность достаётся только тем, кто меньше движения не просил. Сам медиазапрос и то, зачем людям эта настройка, разобраны в прологе — меньше движения по просьбе человека.
Чаще всего задача звучит не «прокрути на 340 пикселей», а «покажи вот этот элемент». Для неё есть element.scrollIntoView() — вся арифметика с координатами остаётся браузеру. Важная деталь: метод прокручивает все прокручиваемые предки, а не только ближайший, поэтому элемент действительно окажется на виду.
Настраивают его два ключа — block (вертикаль) и inline (горизонталь), плюс знакомый behavior. По умолчанию { block: "start", inline: "nearest" }. Самое вежливое значение — "nearest": если элемент и так виден, прокрутки не будет вовсе.
| Значение | Куда встанет элемент |
|---|---|
| start | к началу видимой области (сверху / слева) |
| center | по центру видимой области |
| end | к концу видимой области (снизу / справа) |
| nearest | минимальный сдвиг; если виден — не двигаемся |
Классическая беда: block: "start" ставит элемент вплотную к верхней границе, а там висит липкий заголовок. Чинить это смещением в JavaScript не надо — есть CSS-свойство scroll-margin-top на самом элементе. Оно расширяет его «зону прицеливания», и браузер оставит запас сам.
Когда открыт диалог, фон прокручиваться не должен. Базовый приём — overflow: hidden на контейнере, для страницы это document.body.style.overflow = "hidden". Элемент при этом не перестаёт быть окном прокрутки: позиция сохраняется, программный scrollTo продолжает работать — запрещён только жест пользователя.
Две ловушки. Первая: у страницы исчезает полоса прокрутки, видимая ширина растёт, и весь макет дёргается вправо — лечится компенсирующим padding-right. Вторая: прокрутка внутри самой модалки, докрутившись до края, «протекает» на фон — лечится overscroll-behavior: contain на её содержимом.
Ширину полосы прокрутки считают как разницу между window.innerWidth (вместе с полосой) и document.documentElement.clientWidth (без неё). Нулю она равна там, где полоса накладывается поверх содержимого — например на телефонах, и тогда компенсация просто ничего не сдвинет.
Событие scroll вешается на прокручиваемый элемент, а для страницы — на window или document. У него два характерных свойства. Во-первых, оно неотменяемое: preventDefault внутри обработчика ничего не остановит, прокрутка уже случилась. Во-вторых, оно приходит не мгновенно после изменения scrollTop, а перед отрисовкой ближайшего кадра.
Отсюда правило: в обработчике только запоминаем позицию, а всё тяжёлое — чтение размеров, классы, анимации — делаем внутри requestAnimationFrame, иначе страница начнёт заикаться. А если вопрос звучит как «виден ли элемент» — бесконечная лента, ленивая загрузка, подсветка пункта меню — бери IntersectionObserver: он решает ту же задачу вообще без обработчика прокрутки.
scrollTopу элемента иwindow.scrollYу страницы — одна и та же величина; браузер сам обрезает её до диапазона от нуля доscrollHeight - clientHeight.scrollToзадаёт позицию,scrollByсдвигает от текущей; обе понимают объект{ top, left, behavior }.behavior: "smooth"анимирует прокрутку: сразу после вызова позиция ещё старая, конец ловится событиемscrollend;"instant"перебивает CSS-свойствоscroll-behavior.scrollIntoViewизбавляет от арифметики:blockиinlineрешают, куда встанет элемент, аscroll-marginоставляет запас под липкую шапку.- Замок прокрутки — это
overflow: hiddenплюс компенсация ширины полосы; событиеscrollприходит перед кадром и не отменяется, а «виден ли элемент» лучше спрашивать уIntersectionObserver.
1/3Что выведет box.scrollTo({ top: 300, behavior: "smooth" }); console.log(box.scrollTop);, если блок был в самом верху?
2/3Элементу, у которого максимум прокрутки 400, присвоили el.scrollTop = 999999. Что покажет чтение el.scrollTop?
3/3Открывая модалку, ставим document.body.style.overflow = "hidden". Что чаще всего ломается визуально?
Долистать ленту до низа
Есть лента сообщений в блоке с прокруткой. Напиши atBottom(el) — отвечает true, когда элемент долистан до самого низа (допуск в один пиксель, потому что позиция бывает дробной), и toBottom(el) — мгновенно доводит его донизу.
Максимум прокрутки — это scrollHeight - clientHeight. Значит, «сколько осталось до низа» считается как scrollHeight - scrollTop - clientHeight. Сравнивать с нулём строго нельзя: при дробном масштабе экрана остаток бывает вроде 0.4, поэтому берём допуск.
В toBottom можно передать любое заведомо большое число — браузер обрежет его до максимума. Передавать el.scrollHeight удобнее: и заведомо больше нужного, и читается как намерение.
Плавно, но уважительно
Напиши pickBehavior(), который возвращает "instant", если в системе включена настройка «меньше движения», и "smooth" в остальных случаях. Функция goTo уже готова и должна заработать без изменений.
Системную настройку читают тем же медиазапросом, что и в CSS, только из JavaScript: window.matchMedia возвращает объект со свойством matches.
Тот же объект умеет сообщать об изменении настройки на лету через addEventListener("change", handler). Но для разовой прокрутки проще спрашивать matches прямо в момент вызова — так значение всегда свежее.