Делаем крутой 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; // свойства, которые анимируются
 }
 // ...

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


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

Источник: https://habr.com/ru/post/474254/


Интересные статьи

Интересные статьи

Подслушивание личных разговоров — одна из самых распространённых и неприятных угроз приватности. Не зря многие заклеивают микрофоны и камеры на ноутбуках, чтобы сохранить в секрете са...
Всем привет. Если вы когда-либо работали с универсальными списками в Битрикс24, то, наверное, в курсе, что страница детального просмотра элемента полностью идентична странице редак...
Я тружусь в компании Mutual. Она работает в Бразилии, в сфере равноправного кредитования. Мы помогаем заёмщикам и заимодавцам наладить связь друг с другом. Первые ищут хорошие ставки,...
Привет, Хабр. Данный текст можно считать продолжением статьи "Разбираем звук Dial-up модема", в которой разбирался метод установки связи между модемами. Сегодня мы пойдем дальше, и п...
Если вы пытаетесь разобраться, какой из этих двух замечательных фреймворков выбрать, читайте мнение сторонника Vue по этому вопросу, которое я перевел и дополнил. Переведено при поддержке...