Есть много разных библиотек для реализации слайдера со всеми возможными эффектами. Для 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; // свойства, которые анимируются
}
// ...
Ну вот и все, наш эффект готов! Готовый пример можете посмотреть на моем гитхабе.
Спасибо за внимание!