Есть много разных библиотек для реализации слайдера со всеми возможными эффектами. Для React одни из лучших это: ReactSlick и Swiper. Но когда для моего проекта потребовался горизонтальный sticky-эффект, то ничего подходящего не нашлось.

В этой статье мы попробуем поэтапно создать такой слайдер, возможно он вам тоже понадобится!
Установка необходимых пакетов
Для создания проекта будем использовать Create React App
Создаем приложение:
npx create-react-app my-app
Слайдер мы будем делать не с нуля, а возьмем библиотеку Swiper, там наиболее подходящие события, к которым нужно будет подвязаться (об этом позже). Тогда нам нужно будет установить следующие пакеты:
npm i swiper react-id-swiper
И последний пакет (по желанию), чтобы использовать предпроцессор sass:
npm i node-sass
Получился такой package.json:
package.json
{ "name": "sticky-slider", "version": "0.1.0", "private": true, "dependencies": { "node-sass": "^4.13.0", "react": "^16.11.0", "react-dom": "^16.11.0", "react-id-swiper": "^2.3.2", "react-scripts": "3.2.0", "swiper": "^5.2.0" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": "react-app" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } }
Отлично, теперь приступаем к реализации слайдера.
Создаем простой слайдер
Начнем с того, что создадим небольшой файлик с нашими слайдами.
src/data.json
[ { "title": "Slide 1", "color": "#aac3bf" }, { "title": "Slide 2", "color": "#c9b1bd" }, { "title": "Slide 3", "color": "#d5a29c" }, { "title": "Slide 4", "color": "#82a7a6" }, { "title": "Slide 5", "color": "#e6af7a" }, { "title": "Slide 6", "color": "#95be9e" }, { "title": "Slide 7", "color": "#97b5c5" } ]
После этого слелаем обычный слайдер с эффектами по умолчанию.
// src/components/StickySlider/StickySlider.jsx import React from 'react'; import Swiper from 'react-id-swiper'; import 'react-id-swiper/lib/styles/css/swiper.css'; import data from '../../data'; const StickySlider = () => { const params = { slidesPerView: 3, }; return ( <Swiper {...params}> {data.map((item, idx) => ( <div key={idx}> {item.title} </div> ))} </Swiper> ); }; export default StickySlider;
И соответственно создаем индексный файл для компонента.
// src/components/StickySlider/index.js export { default } from './StickySlider';
Единственный параметр, который мы описали – это slidesPerView (количество видимых слайдов). Нам больше ничего не понадовится, но все возможные параметры свайпера можно посмотреть здесь.
Создадим отдельно компонет Slide, чтобы внешний вид слайдера уже был готов.
// src/components/Slide/Slide.jsx import React from 'react'; import css from './Slide.module.scss'; const Slide = ({ children, color }) => { return ( <div className={css.container}> <div className={css.content} style={{ background: color }} /> <footer className={css.footer}> {children} </footer> </div> ); }; export default Slide;
Стили для слайда.
// src/components/Slide/Slide.module.scss .container { margin: 0 1em; border-radius: 4px; overflow: hidden; background-color: #fff; } .content { box-sizing: border-box; padding: 50% 0; } .footer { color: #333; font-weight: 700; font-size: 1.25em; text-align: center; padding: 1em; }
И соответственно индексный файл:
// src/components/Slide/index.js export { default } from './Slide';
И немного обновим StickySlider.
// src/components/StickySlider/StickySlider.jsx import React from 'react'; import Swiper from 'react-id-swiper'; import 'react-id-swiper/lib/styles/css/swiper.css'; import Slide from '../Slide'; import data from '../../data'; const StickySlider = () => { const params = { slidesPerView: 3, }; return ( <Swiper {...params}> {data.map((item, idx) => ( <div key={idx}> {/* добавляем слайд */} <Slide color={item.color}> {item.title} </Slide> </div> ))} </Swiper> ); }; export default StickySlider;
Теперь вставим этот слайдер в App.jsx, заодно заложим минимальную структуру страницы.
// App.jsx import React from 'react'; import StickySlider from './components/StickySlider'; import css from './App.module.scss'; const App = () => { return ( <div className={css.container}> <h1 className={css.title}>Sticky slider</h1> <div className={css.slider}> <StickySlider /> </div> </div> ); }; export default App;
И в соответствующем scss-файле напишем немного стилей.
// App.module.scss .container { padding: 0 15px; } .title { font-weight: 700; font-size: 2.5em; text-align: center; margin: 1em 0; } .slider { margin: 0 -15px; }
Пока у нас получился такой слайдер:

Круто, начало положено, дальше будем делать из этого слайдера то, что нам нужно.
Добавляем sticky-эффект
У свайпера есть два нужных нам события setTranslate и setTransition.
| Свойство | Когда срабатывает | Что возвращает |
|---|---|---|
setTranslate |
срабатывает когда мы двигаем слайдер и в тот момент, когда опускаем его | возвращает значение, на которое слайдер сдвинут в текущий момент, а в случае когда отпускаем — значение, до которого он будет автоматически доведен |
setTransition |
срабатывает когда мы начинаем двигать слайдер, когда мы отпускаем его и когда слайдер доводится на нужную позицию | возвращает значание transition в милисикундах |
Добавим это в наш компонент StickySlider и сразу пробросим в Slider, там нам это пригодится:
// src/components/StickySlider/StickySlider.jsx import React, { useState, useEffect } from 'react'; import Swiper from 'react-id-swiper'; import 'react-id-swiper/lib/styles/css/swiper.css'; import Slide from '../Slide'; import data from '../../data'; const StickySlider = () => { const [swiper, updateSwiper] = useState(null); const [translate, updateTranslate] = useState(0); const [transition, updateTransition] = useState(0); const params = { slidesPerView: 3, }; useEffect(() => { if (swiper) { swiper.on('setTranslate', (t) => { updateTranslate(t); }); swiper.on('setTransition', (t) => { updateTransition(t); }); } }, [swiper]); return ( <Swiper getSwiper={updateSwiper} {...params}> {data.map((item, idx) => ( <div key={idx}> <Slide translate={translate} transition={transition} color={item.color} > {item.title} </Slide> </div> ))} </Swiper> ); }; export default StickySlider;
Советую подвигать слайдер и посмотреть деталенее что выводится в этом моменте:
// src/components/StickySlider/StickySlider.jsx // ... useEffect(() => { if (swiper) { swiper.on('setTranslate', (t) => { console.log(t, 'translate'); updateTranslate(t); }); swiper.on('setTransition', (t) => { console.log(t, 'transform'); updateTransition(t); }); } }, [swiper]); // ..
Я для хранения состояния использую хуки. Если вы с ними мало знакомы, то советую почитать документацию (на русском).
Далее все самое сложное будет происходить в компоненте Slide.
Нам потребуются состояния отступа от левой границы слайдера и ширина текущего слайда:
// src/components/StickySlider/StickySlider.jsx // ... const container = useRef(null); const [offsetLeft, updateOffsetLeft] = useState(0); const [width, updateWidth] = useState(1); // ...
Они добаляются один раз при инициализации элемента и не изменяются. Поэтому используем useEffect с пустым массивом. При этом достаем параметры не самого слайда а его техническую обертку через parentElement, так как текущую обертку будем преобразовывать при помощи свойства transform.
// src/components/StickySlider/StickySlider.jsx // ... useEffect(() => { setTimeout(() => { const parent = container.current.parentElement; updateOffsetLeft(parent.offsetLeft); updateWidth(parent.offsetWidth); }, 0); }, []); // ...
Самый главный момент. Считаем все это дело и прокидываем в стили:
// src/components/Slide/Slide.jsx // ... const x = -translate - offsetLeft; const k = 1 - x / width; // [0 : 1] const style = x >= -1 ? { transform: `translateX(${x}px) scale(${k * 0.2 + 0.8})`, // [0.8 : 1] opacity: k < 0 ? 0 : k * 0.5 + 0.5, // [0.5 : 1] transition: `${transition}ms`, } : {}; // ...
Свойство translate приходит нам от родителя и оно одинаковое для всех слайдов. Поэтому чтобы найти индивидуальный translate для одного слайда, вычитаем из него offsetLeft.
Переменная k это значение от 0 до 1. С помощью этого значения мы будем делать анимацию. Это ключевая переменная, так как по ней можно делать любые эффекты.
Теперь вычисляем стили. Условие x >= -1 выполняется когда слайд находится в зоне анимации, поэтому при его выполнении навешиваем стили на сайд. Значения scale и opacity пожно подобрать по своему усмотрению. Мне показались наиболее подходящими такие интервалы: [0.8 : 1] для scale и [0.5 : 1] для opacity.
Свойство transition поставляется прямо из события библиотеки.
Вот что получится после добавлению всего вышенаписанного:
// src/components/Slide/Slide.jsx import React, { useRef, useEffect, useState } from 'react'; import css from './Slide.module.scss'; const Slide = ({ children, translate, transition, color }) => { const container = useRef(null); const [offsetLeft, updateOffsetLeft] = useState(0); const [width, updateWidth] = useState(1); useEffect(() => { setTimeout(() => { const parent = container.current.parentElement; updateOffsetLeft(parent.offsetLeft); updateWidth(parent.offsetWidth); }, 0); }, []); const x = -translate - offsetLeft; const k = 1 - x / width; // [0 : 1] const style = x >= -1 ? { transform: `translateX(${x}px) scale(${k * 0.2 + 0.8})`, // [0.8 : 1] opacity: k < 0 ? 0 : k * 0.5 + 0.5, // [0.5 : 1] transition: `${transition}ms`, } : {}; return ( <div ref={container} style={style} className={css.container}> <div className={css.content} style={{ background: color }} /> <footer className={css.footer}> {children} </footer> </div> ); }; export default Slide;
Теперь добавим добавим в файл стилей слайда следующие свойства:
// src/components/Slide/Slide.module.scss .container { // ... transform-origin: 0 50%; // для трансформации относительно левого края transition-property: opacity, transform; // свойства, которые анимируются } // ...
Ну вот и все, наш эффект готов! Готовый пример можете посмотреть на моем гитхабе.
Спасибо за внимание!
