Плавная прокрутка страницы на JavaScript
Вступление.
Во многих продающих лендингах используется плавная прокрутка страницы к определённому тематическому блоку, поэтому тема плавной прокрутки неоднократно рассматривалась во многих статьях, размещённых на сайтах или блогах посвящённых программированию на JavaScript. Предлагались разные варианты решения, как на чистом JavaScript, так и с использованием jQuery. Большинство из них использует anchor
(якоря) и ссылки указывающие на них, что вызывает много споров в комментариях о целесообразности такого метода с точки зрения СЕО. Есть варианты, в которых связь между элементом навигации и блоком, к которому нужно прикрутить страницу, осуществляется через ID
. Этот способ более интересный, но и он не лишён недостатков.
В своей статье я предлагаю рассмотреть вариант JS-скрипта, который не будет использовать ни якоря, ни ID. Идентификация контейнеров, которые необходимо прокрутить до верхнего края экрана, будет основана на индексе элемента в коллекции. Это значительно упростит как HTML-вёрстку, так и JS-код, но при этом, сохраниться их гибкость и упростится добавление новых контейнеров.
HTML-разметка для плавной прокрутки страницы.
Шапка страницы, в которой будет размещено меню с управляющими прокруткой элементами, будет иметь свойство display: fixed
. Такое позиционирование позволит шапке постоянно находиться в верхней части экрана, что обеспечит доступ к элементам навигации.
За основу меню взят немаркированный список <ul>
, а элементами управления будут <span>
. Использование <span>
в качестве интерактивного элемента устраняет ещё одну довольно распространённую ошибку — использование для этих целей тэга <a>
(ссылки).
Как с точки зрения семантики, так и с точки зрения СЕО — тэг
<a>
должен использоваться только для формирования ссылок, ведущих на другие страницы сайта или другой интернет-ресурс. Для управления элементами текущей страницы (показать / скрыть, изменить стиль, переместить, подгрузить и т.д.) должны использоваться элементы <span>
, <button>
, <div>
, <li>
. Именно на них вешаются обработчики событий.
Исходный код разметки HTML:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<div class="header"> <div> <div class="headline"> <h1>Плавное прокручивание страницы</h1> </div> <ul class="menu"> <li><span>Container 1</span></li> <li><span>Container 2</span></li> <li><span>Container 3</span></li> <li><span>Container 4</span></li> <li><span>Container 5</span></li> <li><span>Container 6</span></li> </ul> </div> </div> <div class="wrap"> <div class="box1"><h2>Container 1</h2></div> <div class="box2"><h2>Container 2</h2></div> <div class="box3"><h2>Container 3</h2></div> <div class="box4"><h2>Container 4</h2></div> <div class="box5"><h2>Container 5</h2></div> <div class="box6"><h2>Container 6</h2></div> </div> |
Как видно из HTML-вёрстки, я не использовал ни якоря, ни ссылки на них, ни идентификаторы.
Таблица стилей для плавной прокрутки страницы.
Небольшая таблица стилей, которая определяет внешний вид шапки, меню и самих контейнеров с контентом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
/* reset */ html, body, div, span, h1, h2, ul, li { margin: 0; padding: 0; } *, :after, :before { -webkit-box-sizing: border-box; box-sizing: border-box; } body { font-weight: normal; line-height: 20px; font-family: Roboto, 'Helvetica CY', Verdana, sans-serif; color: #333; padding-top: 95px; } h1 { font-weight: normal; font-size: 24px; color: #e1e1e1; } h2 { font-weight: normal; font-size: 20px; color: #fafafa; } /* header */ .header { width: 100%; background: #1A1A1C; position: fixed; left: 0; top: 0; z-index: 100; border-bottom: solid 1px #181818; } .header > div { width: 1024px; height: 94px; margin: 0 auto; } .headline { height: 61px; padding-top: 21px; border-bottom: solid 1px #38383B; } .menu { display: inline-block; list-style: none; overflow: hidden; } .menu li { height: 33px; line-height: 32px; text-align: center; float: left; } .menu li + li { margin-left: 20px; } .menu span { font-size: 12px; color: #999; text-transform: uppercase; cursor: pointer; -webkit-transition: all 0.3s; transition: all 0.3s; -moz-user-select: none; -webkit-user-select: none; } .menu span:hover, .menu .active { color: #fff; } /* content */ .wrap { width: 1024px; margin: 0 auto; } .wrap > div { margin-top: 1px; padding: 50px; } .box1 { height: 770px; background: #919191; } .box2 { height: 710px; background: #A4A4A4; } .box3 { height: 750px; background: #B6B6B6; } .box4 { height: 800px; background: #C9C9C9; } .box5 { height: 620px; background: #DCDCDC; } .box6 { height: 315px; background: #ebebeb; } .box5 h2, .box6 h2 { color: #a7a7a7; } |
JavaScript для управления плавным прокручиванием страницы.
Для ограничения области видимости, разместим скрипт в анонимной самозапускающейся функции.
1 2 3 4 5 6 |
;(function() { 'use strict'; })(); |
При создании анимации на JavaScript вместо функции
setInterval
используйте функцию requestAnimationFrame
. Эта функция позволяет синхронизировать анимацию со встроенными в браузер механизмами обновления страницы. Результатом будет более эффективное использование графического ускорителя, исключена повторная обработка одних и тех же участков страницы, меньше будет загрузка процессора и, самое главное, анимация будет более плавная, без рывков и дёрганий.
Исходя из выше написанного, первое что нам нужно сделать — это создать возможность кроссбраузерного использование функции requestAnimationFrame
. Добавим вовнутрь созданной анонимной функции следующий код:
1 2 3 4 5 6 7 8 9 |
// обеспечиваем короссбраузерноть для использования встроенного // в браузеры API requestAnimationFrame var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; window.requestAnimationFrame = requestAnimationFrame; |
Теперь необходимо получить ряд объектов и коллекций элементов на основе которых и будет работать наш JavaScrit:
- объект
menu
— для делегирования, основанного на всплытии событий; - коллекция объектов
SPAN
— интерактивные элементы, запускающие прокручивание страницы; - коллекция объектов
DIV
— контейнеры, до которых прокручивается страница.
1 2 3 4 5 6 7 8 9 |
// получаем объект menu var menu = document.querySelector('.menu'), // коллекция объектов SPAN, которые используются, как // управляющие элементы для прокручивания страницы items = menu.querySelectorAll('span'), // коллекция объектов DIV, до которых прокручивается страница containers = document.querySelectorAll('.wrap > div'); |
Теперь нам нужно повесить обработчик события click
на объект menu
. При срабатывании события (по меню был сделан клик) и используя делегирование, основанное на всплытии событий, определяем, что клик произошел именно по элементу span
, а не по пустой области меню. После этого запускаем по очереди две функции: switchLinks
и selectContainer
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
menu.onclick = function(e) { // используя делегирование основанное на всплытии событий, // находим элемент SPAN по которому был сделан клик if (e.target.tagName != 'SPAN') return; // переключаем элемент SPAN в активное состояние и // получаем его индекс в составе коллекции 'items' var current = switchLinks(e.target); // на основании полученного индекса находим DIV из // коллекции 'containers', который и будем прокручивать // до верхнего края экрана // из этой же функции запускаем скролл страницы selectContainer(current); } |
Рассмотрим функцию switchLinks
, отвечающую за подсветку элемента меню, по которому сделан клик. Кроме этого, она возвращает index (current
) данного элемента в коллекции items
. Этот индекс понадобиться для идентификации контейнера, до которого будет прокручиваться страница.
Аргументом функции является объект элемента span
, по которому был сделан клик. Кроме полученного аргумента, функция switchLinks
использует коллекцию объектов items
. Данная коллекция не передаётся в функцию явно в качестве аргумента, т. к. она является глобальной в пределах области видимости самозапускающейся анонимной функции.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
function switchLinks(el) { var current; // перебираем коллекцию элементов SPAN [].forEach.call(items, function(item, index) { // у каждого элемента удаляем класс 'active', // если он был прописан item.classList.remove('active'); // если элемент в текущей итерации совпадает с // элементом, по которому был сделан клик, то // добавляем ему класс 'active' if (item === el) { item.classList.add('active'); // запоминаем индекс этого элемента // по этому индексу будет найден DIV из коллекции // containers, к которому применим анимацию current = index; } }); return current; } |
Теперь разберём JS-код функции selectContainer
. Данная функция находит контейнер из коллекции containers
, используя аргумент current
— индекс активного элемента меню и запускает анимированное прокручивание страницы, вызывая функцию scroll
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
function selectContainer(current) { // перебираем коллекцию элементов DIV [].forEach.call(containers, function(container, index) { // индекс элемента в текущей итерации совпадает // с полученным ранее индексом элемента меню, по // которому был сделан клик if (index == current) { // Y-координата верхней границы выбранного элемента относительно // верхнего края окна браузера с учётом высоты шапки var startY = container.getBoundingClientRect().top - 96, // направление прокрутки зависит от положения верхней границы контейнера // относительно верхней границы окна браузера // нужный нам контейнер может находится выше или ниже окна браузера, // соответственно, страницу нужно прокручивать вверх или вниз // для этого необходимо вычислить коэффициент, от которого будет зависеть // направление прокрутки direction = (startY < 0) ? -1 : (startY > 0) ? 1 : 0; // верхняя граница контейнера, к которому собираемся перейти, находится // сразу под шапкой - нет необходимости прокручивать страницу if (direction == 0) return; // запускаем функцию прокручивания страницы до выбранного контейнера scroll(container, direction); } }); } |
Осталось рассмотреть работу основной функции нашего скрипта — scroll
, которая обеспечивает плавную прокрутку страницы до выбранного контейнера, учитывая направление прокрутки.
Функция принимает два аргумента:
1. container
— объект элемента DIV
до которого будет прокручиваться страница;
2. direction
— коэффициент, определяющий вверх или вниз необходимо прокручивать страницу.
Теперь подробно, по шагам разберёмся, как будет работать анимация прокрутки страницы.
-
1
Перед запуском анимации необходимо назначить несколько переменных и присвоить им значения:
—duration
, длительность анимации;
—start
, время начала анимации;1234var duration = 2000,start = new Date().getTime(); -
2
Внутри функции
scroll
создадим именованную функциюfn
, которая будет рекурсивно вызывать себя до тех пор, пока не закончится анимация. Внутри этой функции объявим и присвоим значения ещё трём переменным:
—top
, текущее положение верхней границы контейнера с учётом высоты шапки с меню;
—now
, время прошедшее от начала анимации прокрутки страницы;
—result
, величина прокрутки страницы за один цикл функцииfn
.1234567var fn = function() {var top = el.getBoundingClientRect().top - 96,now = new Date().getTime() - start,result = Math.round(top * now / duration);}где,
96
— высота шапки. -
3
Для того, чтобы контейнер не проскочил нижнюю границу шапки при прокручивании страницы, необходимо корректировать значение
result
в зависимости от текущего положения верхней границы контейнера. Для этого добавим несколько условий:123result = (result > direction * top) ? top : (result == 0) ? direction : result; -
4
Сравниваем произведение
direction * top
c0
. Если произведение больше нуля, то прокручиваем страницу на величинуresult
и рекурсивно запускаем функциюfn
на очередной цикл.123456if (direction * top > 0) {window.scrollBy(0,result);requestAnimationFrame(fn);}При прокручивании страницы вниз значения
direction
иtop
— положительные, при прокручивании вверх — отрицательные, поэтому результат умножения всегда положительный.Метод
scrollBy(x,y)
прокручивает страницу относительно текущих координат, соответственноwindow.scrollBy(0,result)
прокрутит страницу по координате ‘Y’ на величину result. -
5
Для старта анимации, необходимо в конце функции
scroll
разместить код первоначально вызывающий функциюfn
.
Теперь соберём всё вместе и посмотрим, как выглядит функция scroll
полностью:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
function scroll(el, direction) { // длительность прокручивания страницы var duration = 2000, // время начала анимации start = new Date().getTime(); var fn = function() { // текущее положение верхней границы контейнера с учётом высоты шапки с меню // при прокрутке контейнер не должен заходить под шапку var top = el.getBoundingClientRect().top - 96, // время прошедшее от начала анимации прокручивания страницы now = new Date().getTime() - start, // величина прокрутки страницы за один цикл функции 'fn' result = Math.round(top * now / duration); // корректируем значение 'result', чтобы контейнер остановился // точно по нижней границе шапки result = (result > direction * top) ? top : (result == 0) ? direction : result; // если верхняя граница контейнера не достигла нижней границы шапки if (direction * top > 0) { // прокручиваем страницу на величину result window.scrollBy(0,result); // рекурсивно запускаем функцию анимации прокрутки страницы requestAnimationFrame(fn); } } // старт анимации прокрутки страницы requestAnimationFrame(fn); } |
Вроде бы задача решена, но… Если последний блок имеет высоту меньше высоты экрана, как в нашем примере, то его верхняя граница физически не сможет достигнуть шапки, другими словами, условие
1 2 3 |
if (direction * top > 0) { ... } |
всегда будет истинно и наш скрипт зациклится.
Давайте внимательно рассмотрим рисунок:
Как видно из рисунка, когда страница прокручена полностью, то разность между высотой страницы и высотой прокрутки равна видимой части окна браузера, в противном случае — эта разность больше. Вот эту зависимость и будем использовать для выхода из цикла плавной прокрутки страницы.
Чтобы реализовать эту зависимость в качестве условия прекращения работы скрипта, необходимо получить следующие значения:
-
Высота документа (страницы).
Определить размер страницы с учетом прокрутки можно, взяв максимум из нескольких свойств:1234567var pageHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight,document.body.offsetHeight, document.documentElement.offsetHeight,document.body.clientHeight, document.documentElement.clientHeight);Добавим этот код в начале нашей анонимной функции, после строки:
1234// коллекция объектов DIV, до которых прокручивается страницаcontainers = document.querySelectorAll('.wrap > div'); -
Текущая прокрутка страницы
Текущую прокрутку можно получить, используя специальное свойствоwindow.pageYOffset
-
Высота видимой части окна
Высота видимой части окна содержится в свойствеdocument.documentElement.clientHeigh
Обновим функцию scroll
с учётом выше написанного:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
function scroll(el, direction) { // длительность прокручивания страницы var duration = 2000, // старт анимации прокручивания страницы start = new Date().getTime(); var fn = function() { // текущее положение верхней границы контейнера с учётом высоты шапки с меню // при прокрутке контейнер не должен заходить под шапку var top = el.getBoundingClientRect().top - 96, // время прошедшее от начала прокрутки страницы now = new Date().getTime() - start, // на сколько должна быть прокручена страница result = Math.round(top * now / duration); // корректируем значение 'result', чтобы контейнер остановился // точно по нижней границе шапки result = (result > direction * top) ? top : (result == 0) ? direction : result; // определяем есть необходимость прокручивать страницу дальше или нет // применение этого условия необходимо, когда высота последнего контейнера // меньше высоты экрана и верхняя граница контейнера физически не может // достигнуть верхней границы экрана, в нашей вёрстке - это container 6 // window.pageYOffset - текущая прокрутка страницы // document.documentElement.clientHeigh - размер видимой части окна if (direction * top > 0 && (pageHeight - window.pageYOffset) > direction * document.documentElement.clientHeight) { window.scrollBy(0,result); // рекурсивно запускаем функцию анимации прокрутки страницы requestAnimationFrame(fn); } } // старт прокрутки страницы requestAnimationFrame(fn); } |
Итак, скрипт плавной прокрутки страницы работает, кажется, все нюансы учтены и на этом можно остановиться.
Комментарии
-
Комментарии должны содержать вопросы и дополнения по статье, ответы на вопросы других пользователей.
Комментарии содержащие обсуждение политики, будут безжалостно удаляться. -
Для удобства чтения Вашего кода, не забываейте его форматировать. Вы его можете подсветить код с помощью тега
<pre>
:
—<pre class="lang:xhtml">
- HTML;
—<pre class="lang:css">
- CSS;
—<pre class="lang:javascript">
- JavaScript. - Если что-то не понятно в статье, постарайтесь указать более конкретно, что именно не понятно.
-
-
Нельзя регулировать длительность анимации, да и не всеми, даже современными, браузерами поддерживается.
А так, конечно, если устраивает «smooth», то лучше использовать scrollIntoView — значительно сократится и упростится js-код.
-
-
Спасибо! выручили!
-
Почему то, если покликать быстро на разные ссылки подряд, она зависает и замирает в конце. Даже на вашем демо.
Скролл колесом мышки становится нерабочей, да и по полосе она замирает.
Попробуйте сами.
Привет
А чем scrollIntoView не нравится?
element.scrollIntoView({behavior: «smooth»});
https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView