Создайте собственный полностраничный слайдер с помощью CSS и JavaScript
Опубликовано: 2022-03-11Я много работаю с пользовательскими полноэкранными макетами, практически ежедневно. Обычно эти макеты подразумевают значительное количество взаимодействий и анимации. Будь то сложная временная шкала переходов или управляемый пользователем набор событий на основе прокрутки, в большинстве случаев пользовательский интерфейс требует большего, чем просто использование стандартного плагина с несколькими настройками и изменениями. . С другой стороны, я вижу, что многие разработчики JavaScript, как правило, используют свой любимый плагин JS, чтобы упростить свою работу, даже если задача может не требовать всех наворотов, которые предоставляет определенный плагин.
Отказ от ответственности: использование одного из множества доступных плагинов, конечно же, имеет свои преимущества. Вы получите множество опций, которые вы можете использовать для настройки в соответствии со своими потребностями без необходимости много кодировать. Кроме того, большинство авторов плагинов оптимизируют свой код, делают его кроссбраузерным и кроссплатформенным и так далее. Но, тем не менее, вы получаете полноразмерную библиотеку, включенную в ваш проект, возможно, только для одной или двух разных вещей, которые она предоставляет. Я не говорю, что использование стороннего плагина любого рода — это, естественно, плохо, я делаю это ежедневно в своих проектах, просто обычно полезно взвесить все за и против каждого подхода. хорошая практика в кодировании. Когда дело доходит до того, чтобы делать свои собственные вещи таким образом, требуется немного больше знаний и опыта программирования, чтобы знать, что вы ищете, но в конце концов вы должны получить фрагмент кода, который делает одну вещь и только одну вещь таким образом. вы этого хотите.
Цель этой статьи — показать чистый CSS/JS-подход к разработке полноэкранного макета слайдера, активируемого прокруткой, с пользовательской анимацией контента. В этом упрощенном подходе я расскажу об базовой структуре HTML, которую вы ожидаете получить от серверной части CMS, современных методах компоновки CSS (SCSS) и стандартном кодировании JavaScript для полной интерактивности. Будучи голой, эта концепция может быть легко расширена до более крупного плагина и/или использована в различных приложениях, не имеющих никаких зависимостей в своей основе.
Дизайн, который мы собираемся создать, представляет собой минималистическую витрину портфолио архитектора с избранными изображениями и названиями каждого проекта. Полный слайдер с анимацией будет выглядеть так:
Вы можете проверить демо здесь, и вы можете получить доступ к моему репозиторию Github для получения дополнительной информации.
Обзор HTML
Итак, вот базовый HTML-код, с которым мы будем работать:
<div> <div class="mask"> <!-- Textual logo will go here --> </div> <div> <div class="slides"> <!-- Featured image slides will go here --> </div> <div class="slides mask"> <!-- Slide titles will go here --> </div> </div> <div> <!-- Static info on the right --> </div> <nav> <!-- Current slide indicator --> </nav> </div>
Div с id hero-slider
— наш основной держатель. Внутри макет разделен на разделы:
- Логотип (статический раздел)
- Слайд-шоу, над которым мы будем работать в основном
- Информация (статический раздел)
- Навигация по слайдеру, указывающая текущий активный слайд, а также общее количество слайдов.
Давайте сосредоточимся на разделе слайд-шоу, так как это нас интересует в этой статье. Здесь у нас две части — основная и вспомогательная . Main — это div, который содержит избранные изображения, а aux содержит заголовки изображений. Структура каждого слайда внутри этих двух держателей довольно проста. Здесь у нас есть слайд с изображением внутри основного держателя:
<div class="slide" data-index="0"> <div class="abs-mask"> <div class="slide-image"> </div> </div> </div>
Атрибут данных индекса — это то, что мы будем использовать для отслеживания того, где мы находимся в слайд-шоу. Раздел «abs-mask» мы будем использовать для создания интересного эффекта перехода, а раздел «слайд-изображение» содержит конкретное изображение. Изображения визуализируются как встроенные, как если бы они поступали непосредственно из CMS и настраивались конечным пользователем.
Точно так же заголовок скользит внутри вспомогательного держателя:
<h2 class="slide-title slide" data-index="0"><a href="#">#64 Paradigm</a></h2>
Каждый заголовок слайда представляет собой тег H2 с соответствующим атрибутом данных и ссылкой, позволяющей перейти на единственную страницу этого проекта.
Остальная часть нашего HTML также довольно проста. У нас есть логотип вверху, статическая информация, которая сообщает пользователю, на какой странице он находится, некоторое описание и индикатор текущего/общего ползунка.
Обзор CSS
Исходный код CSS написан в SCSS, препроцессоре CSS, который затем компилируется в обычный CSS, который может интерпретировать браузер. SCSS дает вам преимущество использования переменных, вложенного выбора, примесей и других интересных вещей, но его необходимо скомпилировать в CSS, чтобы браузер читал код должным образом. Для целей этого руководства я использовал Scout-App для обработки компиляции, так как хотел иметь минимальный набор инструментов.
Я использовал flexbox для обработки основного макета бок о бок. Идея состоит в том, чтобы слайд-шоу было с одной стороны, а информационный раздел — с другой.
#hero-slider { position: relative; height: 100vh; display: flex; background: $dark-color; } #slideshow { position: relative; flex: 1 1 $main-width; display: flex; align-items: flex-end; padding: $offset; } #info { position: relative; flex: 1 1 $side-width; padding: $offset; background-color: #fff; }
Давайте углубимся в позиционирование и снова сосредоточимся на разделе слайд-шоу:
#slideshow { position: relative; flex: 1 1 $main-width; display: flex; align-items: flex-end; padding: $offset; } #slides-main { @extend %abs; &:after { content: ''; @extend %abs; background-color: rgba(0, 0, 0, .25); z-index: 100; } .slide-image { @extend %abs; background-position: center; background-size: cover; z-index: -1; } } #slides-aux { position: relative; top: 1.25rem; width: 100%; .slide-title { position: absolute; z-index: 300; font-size: 4vw; font-weight: 700; line-height: 1.3; @include outlined(#fff); } }
Я установил основной ползунок в абсолютное положение, а фоновые изображения растягивают всю область с помощью свойства background-size: cover
. Чтобы обеспечить больший контраст с заголовками слайдов, я установил абсолютный псевдоэлемент, который действует как наложение. Вспомогательный ползунок, содержащий заголовки слайдов, расположен внизу экрана и над изображениями.
Поскольку единовременно будет виден только один слайд, я также установил для каждого заголовка абсолютный размер и рассчитал размер держателя с помощью JS, чтобы убедиться, что нет обрезания, но подробнее об этом в одном из наших следующих разделов. Здесь вы можете увидеть использование функции SCSS, называемой расширением:
%abs { position: absolute; top: 0; left: 0; height: 100%; width: 100%; }
Поскольку я часто использовал абсолютное позиционирование, я превратил этот CSS в расширяемый, чтобы он был легко доступен в различных селекторах. Кроме того, я создал миксин под названием «контурный», чтобы обеспечить СУХОЙ подход при стилизации заголовков и основного заголовка слайдера.
@mixin outlined($color: $dark-color, $size: 1px) { color: transparent; -webkit-text-stroke: $size $color; }
Что касается статической части этого макета, в ней нет ничего сложного, но здесь вы можете увидеть интересный метод позиционирования текста, который должен располагаться по оси Y, а не по его обычному течению:
.slider-title-wrapper { position: absolute; top: $offset; left: calc(100% - #{$offset}); transform-origin: 0% 0%; transform: rotate(90deg); @include outlined; }
Я хотел бы обратить ваше внимание на свойство transform-origin
, так как я обнаружил, что оно действительно недостаточно используется для этого типа макета. Позиционирование этого элемента заключается в том, что его привязка остается в верхнем левом углу элемента, устанавливая точку вращения и обеспечивая непрерывное перемещение текста от этой точки вниз без каких-либо проблем, когда речь идет о разных размерах экрана.
Давайте взглянем на более интересную часть CSS — начальную анимацию загрузки:
Обычно такое синхронизированное поведение анимации достигается с помощью библиотеки — например, GSAP — одна из лучших, предоставляющая отличные возможности рендеринга, простая в использовании и имеющая функциональность временной шкалы, которая позволяет разработчику программно связывать элементы. переходы друг в друга.
Однако, поскольку это чистый пример CSS/JS, я решил пойти здесь очень просто. Таким образом, каждый элемент по умолчанию устанавливается в исходное положение — либо скрыт трансформацией, либо непрозрачностью и отображается при загрузке ползунка, который запускается нашим JS. Все свойства перехода настраиваются вручную, чтобы обеспечить естественный и интересный поток, при этом каждый переход переходит в другой, обеспечивая приятное визуальное впечатление.
#logo:after { transform: scaleY(0); transform-origin: 50% 0; transition: transform .35s $easing; } .logo-text { display: block; transform: translate3d(120%, 0, 0); opacity: 0; transition: transform .8s .2s, opacity .5s .2s; } .current, .sep:before { opacity: 0; transition: opacity .4s 1.3s; } #info { transform: translate3d(100%, 0, 0); transition: transform 1s $easing .6s; } .line { transform-origin: 0% 0; transform: scaleX(0); transition: transform .7s $easing 1s; } .slider-title { overflow: hidden; >span { display: block; transform: translate3d(0, -100%, 0); transition: transform .5s 1.5s; } }
Если есть что-то, что я хотел бы, чтобы вы здесь увидели, так это использование свойства transform
. При перемещении HTML-элемента, будь то переход или анимация, рекомендуется использовать свойство transform
. Я вижу много людей, которые склонны обманывать либо поля, либо отступы, либо даже смещения — верхнее, левое и т. д., что не дает адекватных результатов, когда дело доходит до рендеринга.
Чтобы получить более глубокое представление о том, как использовать CSS при добавлении интерактивного поведения, я не могу не порекомендовать следующую статью.
Это Пол Льюис, инженер Chrome, и охватывает почти все, что нужно знать о рендеринге пикселей в Интернете, будь то CSS или JS.
Обзор JavaScript и логика слайдера
Файл JavaScript разделен на две отдельные функции.
Функция heroSlider
, которая отвечает за всю необходимую нам функциональность, и функция utils
, в которую я добавил несколько повторно используемых служебных функций. Я прокомментировал каждую из этих служебных функций, чтобы предоставить контекст, если вы хотите повторно использовать их в своем проекте.
Основная функция закодирована таким образом, что имеет две ветви: init
и resize
. Эти ветки доступны через возврат основной функции и вызываются при необходимости. init
— это инициализация основной функции, которая запускается при загрузке окна. Точно так же ветвь изменения размера запускается при изменении размера окна. Единственной целью функции изменения размера является пересчет размера ползунка заголовка при изменении размера окна, поскольку размер шрифта заголовка может различаться.
В функции heroSlider
я предоставил объект слайдера, который содержит все данные и селекторы, которые нам понадобятся:
const slider = { hero: document.querySelector('#hero-slider'), main: document.querySelector('#slides-main'), aux: document.querySelector('#slides-aux'), current: document.querySelector('#slider-nav .current'), handle: null, idle: true, activeIndex: -1, interval: 3500 };
В качестве примечания: этот подход можно легко адаптировать, если вы, например, используете React, поскольку вы можете хранить данные в состоянии или использовать недавно добавленные хуки. Чтобы оставаться в теме, давайте просто рассмотрим, что представляет каждая из пар ключ-значение:

- Первые четыре свойства представляют собой HTML-ссылку на элемент DOM, которым мы будем манипулировать.
- Свойство
handle
будет использоваться для запуска и остановки функции автозапуска. - Свойство
idle
— это флаг, который не позволяет пользователю принудительно прокручивать слайд во время перехода. -
activeIndex
позволит нам отслеживать текущий активный слайд -
interval
обозначает интервал автовоспроизведения ползунка
При инициализации слайдера мы вызываем две функции:
setHeight(slider.aux, slider.aux.querySelectorAll('.slide-title')); loadingAnimation();
Функция setHeight
обращается к служебной функции, чтобы установить высоту нашего вспомогательного ползунка на основе максимального размера заголовка. Таким образом мы гарантируем, что будет предоставлен адекватный размер, и ни один заголовок слайда не будет обрезан, даже если его содержание упадет на две строки.
Функция loadingAnimation добавляет класс CSS к элементу, предоставляющему вступительные переходы CSS:
const loadingAnimation = function () { slider.hero.classList.add('ready'); slider.current.addEventListener('transitionend', start, { once: true }); }
Поскольку наш индикатор ползунка является последним элементом на временной шкале перехода CSS, мы ждем окончания его перехода и вызываем функцию запуска. Предоставляя дополнительный параметр в качестве объекта, мы гарантируем, что он сработает только один раз.
Давайте посмотрим на функцию запуска:
const start = function () { autoplay(true); wheelControl(); window.innerWidth <= 1024 && touchControl(); slider.aux.addEventListener('transitionend', loaded, { once: true }); }
Таким образом, когда макет завершен, его начальный переход запускается функцией loadingAnimation
, и функция запуска вступает во владение. Затем он запускает функцию автозапуска, включает управление колесом, определяет, на каком устройстве мы находимся: сенсорное или настольное, и ждет первого перехода слайдов заголовков, чтобы добавить соответствующий класс CSS.
Автовоспроизведение
Одной из основных функций этого макета является функция автозапуска. Давайте рассмотрим соответствующую функцию:
const autoplay = function (initial) { slider.autoplay = true; slider.items = slider.hero.querySelectorAll('[data-index]'); slider.total = slider.items.length / 2; const loop = () => changeSlide('next'); initial && requestAnimationFrame(loop); slider.handle = utils().requestInterval(loop, slider.interval); }
Во-первых, мы устанавливаем флаг автовоспроизведения в значение true, указывая, что ползунок находится в режиме автовоспроизведения. Этот флаг полезен при определении необходимости повторного запуска автовоспроизведения после взаимодействия пользователя с ползунком. Затем мы ссылаемся на все элементы слайдера (слайды), так как мы изменим их активный класс и рассчитаем общее количество итераций, которое будет иметь слайдер, сложив все элементы и разделив на два, поскольку у нас есть два синхронизированных макета слайдера (основной и дополнительный) но только один «ползунок» как таковой, который меняет их оба одновременно.
Самая интересная часть кода здесь — функция цикла. Он вызывает slideChange
, задавая направление скольжения, которое мы рассмотрим через минуту, однако функция цикла вызывается пару раз. Давайте посмотрим, почему.
Если исходный аргумент оценивается как истина, мы будем вызывать функцию цикла как обратный вызов requestAnimationFrame
. Это происходит только при первой загрузке слайдера, которая вызывает немедленную смену слайда. Используя requestAnimationFrame
, мы выполняем предоставленный обратный вызов непосредственно перед перерисовкой следующего кадра.
Однако, поскольку мы хотим продолжать просматривать слайды в режиме автовоспроизведения, мы будем использовать повторный вызов этой же функции. Обычно это достигается с помощью setInterval. Но в данном случае мы воспользуемся одной из служебных функций requestInterval
. В то время как setInterval
будет работать очень хорошо, requestInterval
— это расширенная концепция, основанная на requestAnimationFrame
и обеспечивающая более производительный подход. Это гарантирует, что функция повторно запускается только в том случае, если вкладка браузера активна.
Подробнее об этой концепции в этой замечательной статье можно прочитать в CSS трюках. Обратите внимание, что мы присваиваем возвращаемое этой функцией значение нашему свойству slider.handle
. Этот уникальный идентификатор, который возвращает функция, доступен нам, и мы будем использовать его для отмены автовоспроизведения позже с помощью cancelAnimationFrame
.
Смена слайда
Функция slideChange
является основной во всей концепции. Он меняет слайды, будь то автовоспроизведение или пользовательский триггер. Он знает направление ползунка, обеспечивает зацикливание, поэтому, когда вы дойдете до последнего слайда, вы сможете перейти к первому слайду. Вот как я это закодировал:
const changeSlide = function (direction) { slider.idle = false; slider.hero.classList.remove('prev', 'next'); if (direction == 'next') { slider.activeIndex = (slider.activeIndex + 1) % slider.total; slider.hero.classList.add('next'); } else { slider.activeIndex = (slider.activeIndex - 1 + slider.total) % slider.total; slider.hero.classList.add('prev'); } //reset classes utils().removeClasses(slider.items, ['prev', 'active']); //set prev const prevItems = [...slider.items] .filter(item => { let prevIndex; if (slider.hero.classList.contains('prev')) { prevIndex = slider.activeIndex == slider.total - 1 ? 0 : slider.activeIndex + 1; } else { prevIndex = slider.activeIndex == 0 ? slider.total - 1 : slider.activeIndex - 1; } return item.dataset.index == prevIndex; }); //set active const activeItems = [...slider.items] .filter(item => { return item.dataset.index == slider.activeIndex; }); utils().addClasses(prevItems, ['prev']); utils().addClasses(activeItems, ['active']); setCurrent(); const activeImageItem = slider.main.querySelector('.active'); activeImageItem.addEventListener('transitionend', waitForIdle, { once: true }); }
Идея состоит в том, чтобы определить активный слайд на основе его индекса данных, который мы получили из HTML. Давайте рассмотрим каждый шаг:
- Установите флаг простоя ползунка в false. Это указывает на то, что происходит смена слайдов, а колесико и сенсорные жесты отключены.
- Предыдущий класс CSS направления ползунка сбрасывается, и мы проверяем наличие нового. Параметр направления предоставляется либо по умолчанию как «следующий», если мы исходим из функции автовоспроизведения, либо вызванной пользователем функцией
wheelControl
илиtouchControl
. - Основываясь на направлении, мы вычисляем индекс активного слайда и предоставляем слайдеру CSS-класс текущего направления. Этот класс CSS используется для определения того, какой эффект перехода будет использоваться (например, справа налево или слева направо).
- Слайды сбрасывают свои классы CSS «состояния» (предыдущий, активный) с помощью другой служебной функции, которая удаляет классы CSS, но может быть вызвана в NodeList, а не только в одном элементе DOM. После этого только к предыдущим и текущим активным слайдам добавляются эти классы CSS. Это позволяет CSS ориентироваться только на эти слайды и обеспечивать адекватный переход.
-
setCurrent
— это обратный вызов, который обновляет индикатор ползунка на основе activeIndex. - Наконец, мы ждем окончания перехода активного слайда изображения, чтобы вызвать обратный вызов
waitForIdle
, который перезапускает автовоспроизведение, если оно было ранее прервано пользователем.
Пользовательские элементы управления
В зависимости от размера экрана я добавил два типа пользовательских элементов управления — колесико и сенсорное управление. Управление колесом:
const wheelControl = function () { slider.hero.addEventListener('wheel', e => { if (slider.idle) { const direction = e.deltaY > 0 ? 'next' : 'prev'; stopAutoplay(); changeSlide(direction); } }); }
Здесь мы слушаем даже колесо, и если ползунок в данный момент находится в режиме ожидания (в настоящее время не анимирует смену слайда), мы определяем направление колеса, вызываем stopAutoplay
, чтобы остановить функцию автозапуска, если она выполняется, и меняем слайд в зависимости от направления. Функция stopAutoplay
— это не что иное, как простая функция, которая устанавливает для нашего флага автозапуска значение false и отменяет наш интервал, вызывая служебную функцию cancelRequestInterval
, передавая ей соответствующий дескриптор:
const stopAutoplay = function () { slider.autoplay = false; utils().clearRequestInterval(slider.handle); }
Как и в случае с wheelControl
, у нас есть touchControl
, который отвечает за сенсорные жесты:
const touchControl = function () { const touchStart = function (e) { slider.ts = parseInt(e.changedTouches[0].clientX); window.scrollTop = 0; } const touchMove = function (e) { slider.tm = parseInt(e.changedTouches[0].clientX); const delta = slider.tm - slider.ts; window.scrollTop = 0; if (slider.idle) { const direction = delta < 0 ? 'next' : 'prev'; stopAutoplay(); changeSlide(direction); } } slider.hero.addEventListener('touchstart', touchStart); slider.hero.addEventListener('touchmove', touchMove); }
Мы слушаем два события: touchstart
и touchmove
. Затем вычисляем разницу. Если он возвращает отрицательное значение, мы переходим к следующему слайду, поскольку пользователь провел пальцем справа налево. С другой стороны, если значение положительное, что означает, что пользователь провел пальцем слева направо, мы запускаем slideChange
с направлением, переданным как «предыдущее». В обоих случаях функция автовоспроизведения останавливается.
Это довольно простая реализация пользовательского жеста. Чтобы развить это, мы могли бы добавить кнопки «предыдущий/следующий», чтобы вызывать slideChange
при нажатии, или добавить маркированный список, чтобы перейти непосредственно к слайду на основе его индекса.
Подведение итогов и заключительные мысли о CSS
Итак, вот вам и чистый CSS/JS способ кодирования нестандартного макета слайдера с современными эффектами перехода.
Я надеюсь, что вы найдете этот подход полезным как способ мышления и сможете использовать что-то подобное в своих проектах внешнего интерфейса при написании кода для проекта, который не обязательно был разработан традиционным образом.
Для тех из вас, кто интересуется эффектом перехода изображения, я расскажу об этом в следующих нескольких строках.
Если мы вернемся к HTML-структуре слайдов, которую я предоставил во вступительном разделе, мы увидим, что вокруг каждого слайда изображения есть div
с CSS-классом abs-mask
. Что делает этот div
, так это то, что он скрывает часть видимого изображения на определенную величину, используя overflow:hidden
и смещая его в другом направлении, чем изображение. Например, если мы посмотрим, как закодирован предыдущий слайд:
&.prev { z-index: 5; transform: translate3d(-100%, 0, 0); transition: 1s $easing; .abs-mask { transform: translateX(80%); transition: 1s $easing; } }
Предыдущий слайд имеет смещение -100% по оси X, перемещая его влево от текущего слайда, однако внутренний элемент div abs-mask
смещается на 80% вправо, обеспечивая более узкое окно просмотра. Это, в сочетании с большим z-индексом для активного слайда, приводит к своего рода эффекту покрытия — активное изображение закрывает предыдущее, в то же время расширяя свою видимую область за счет перемещения маски, которая обеспечивает полный обзор.