Делаем крутой sticky-эффект для слайдера на React

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

    Ну вот и все, наш эффект готов! Готовый пример можете посмотреть на моем гитхабе.


    Спасибо за внимание!

    • +10
    • 4,4k
    • 8
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 8

      –2
      import 'react-id-swiper/lib/styles/css/swiper.css';
      2019 год, я не сильно в в этом разбираюсь но это трындец.
        0
        А что в этом плохого? И какие ваши варианты по импорту стилей?
          0
          что произойдет когда стиль переедет выше на каталог? Или вообще в другой каталог? я не знаю как там импорт стилей происходит может там все плохо, но это же работает везде.
            0
            Это нормальная практика так импортировать библиотечные стили в реакте, больше вариантов особо нет. Чисто теоретически конечно может переехать, но только при обновлении версии пакета
              0
              import Swiper from 'react-id-swiper';
              import 'react-id-swiper/lib/styles/css/swiper.css';
              У меня даже слов нет, как это по разному может работать?
                +2

                Прочитал все 3 ваших комментария в ветке, так и не понял, в чем вы здесь видите проблему.


                Насчет возможного переезда файла в другое место — ну, это же не рандомный файл, а часть публичного интерфейса библиотеки, с документированным использованием (https://github.com/kidjp85/react-id-swiper#styling).

        0
        У меня начинает ругаться на const container = useRef(null), т.к. useRef нигде не определён. Подсмотрел в вашем готовом проекте — его там нет.
          0
          Должно быть все нормально. Вы точно его импортировали?

          import React, { useRef, useEffect, useState } from 'react';

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое