Плавная прокрутка страницы на 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 * topc0. Если произведение больше нуля, то прокручиваем страницу на величину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