Слайдер-превью изображений при наведении мыши

На сайтах интернет-магазинов (или тому подобных), где на страницах рубрик представлены карточки товаров или услуг, можно встретить мини-слайдеры, в которых показаны превью этих товаров и услуг. При наведении курсора и перемещении его влево-вправо можно посмотреть несколько таких изображений. Чтобы было более понятно, о чем идет речь, зайдите на сайты ozon.ru и realty.yandex.ru (актуально на момент написания данной статьи).

Хочу поделиться моей реализацией функционала этого слайдера в двух вариантах: на jQuery и в нативном JavaScript.

Из особенностей: на десктопе изображения меняются при перемещении курсора мыши над ним горизонтально, а на мобильных устройствах – если проводить пальцем по изображению.

Исходная структура HTML-кода

<div class="images">
  <img src="image">
  <img src="image">
  <img src="image">
</div>

После выполнения скрипта она обретает следующий вид:

<div class="hvr">
  <div class="hvr__images">
    <div class="images">
      <img class="image">
      <img class="image">
      <img class="image">
    </div>
    <div class="hvr__sectors">
      <div class="hvr__sector"></div>
      <div class="hvr__sector"></div>
      <div class="hvr__sector"></div>
    </div>
  </div>
  <div class="hvr__dots">
    <div class="hvr__dot hvr__dot--active"></div>
    <div class="hvr__dot"></div>
    <div class="hvr__dot"></div>
  </div>
</div>

CSS-код

Есть обязательные стили:

.hvr__images {
  position: relative;
}

.hvr__sectors {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
}

.hvr__sector {
  flex-grow: 1;
}

Также можно добавить допонительные стили:

  • для изображений, чтобы до срабатывания скрипта отображалось только первое изображение;
  • для точек, показывающих общее количество изображений и активный слайд.
.images {
  display: flex;
  overflow: hidden;
}

.image {
  display: block;
}

.hvr__dots {
  display: flex;
  align-items: center;
  justify-content: center;
}

.hvr__dot {
  width: 5px;
  height: 5px;
  margin: 10px 2px 0;
  border-radius: 50%;
  background: #d6dbe0;
}

.hvr__dot--active {
  background: #000;
}

Решение на jQuery

(function ($) {
  $.fn.HvrSlider = function () {
    return this.each(function () {
      var el = $(this);
      if (el.find('img').length > 1) {
        var hvr = $('<div>', {
          class: 'hvr',
          append: [
            $('<div>', {
              class: 'hvr__images',
              append: $('<div>', {
                class: 'hvr__sectors',
              }),
            }),
            $('<div>', {
              class: 'hvr__dots',
            }),
          ],
          insertAfter: el,
          prepend: el,
        });
        var hvrImages = $('.hvr__images', hvr);
        var hvrImage = $('img', hvr);
        var hvrSectors = $('.hvr__sectors', hvr);
        var hvrDots = $('.hvr__dots', hvr);
        el.prependTo(hvrImages);
        hvrImage.each(function () {
          hvrSectors.prepend('<div class="hvr__sector"></div>');
          hvrDots.append('<div class="hvr__dot"></div>');
        });
        $('.hvr__dot:first', hvrDots).addClass('hvr__dot--active');
        var setActiveEl = function (el) {
          hvrImage.hide().eq(el.index()).show();
          $('.hvr__dot', hvrDots).removeClass('hvr__dot--active').eq(el.index()).addClass('hvr__dot--active');
        };
        $('.hvr__sector', hvrSectors).hover(function () {
          setActiveEl($(this));
        });
        hvrSectors.on('touchmove', function (e) {
          var position = e.originalEvent.changedTouches[0];
          var target = document.elementFromPoint(position.clientX, position.clientY);
          if ($(target).is('.hvr__sector')) {
            setActiveEl($(target));
          }
        });
      }
    });
  };
})(jQuery);

Код для инициализации скрипта:

$('.images').HvrSlider();

Результат выглядит следующим образом:

See the Pen
Слайдер-превью изображений на jQuery
by Dimox (@dimox)
on CodePen.

Решение на JavaScript

class HvrSlider {
  constructor(selector) {
    const elements = document.querySelectorAll(selector);
    elements.forEach((el) => {
      if (el.querySelectorAll('img').length > 1) {
        const hvr = document.createElement('div');
        hvr.classList.add('hvr');

        const hvrImages = document.createElement('div');
        hvrImages.classList.add('hvr__images');
        hvr.appendChild(hvrImages);

        const hvrSectors = document.createElement('div');
        hvrSectors.classList.add('hvr__sectors');
        hvrImages.appendChild(hvrSectors);

        const hvrDots = document.createElement('div');
        hvrDots.classList.add('hvr__dots');
        hvr.appendChild(hvrDots);

        el.parentNode.insertBefore(hvr, el);
        hvrImages.prepend(el);

        const hvrImagesArray = hvr.querySelectorAll('img');
        hvrImagesArray.forEach(() => {
          hvrSectors.insertAdjacentHTML('afterbegin', '<div class="hvr__sector"></div>');
          hvrDots.insertAdjacentHTML('afterbegin', '<div class="hvr__dot"></div>');
        });
        hvrDots.firstChild.classList.add('hvr__dot--active');
        const setActiveEl = function (targetEl) {
          const index = [...hvrSectors.children].indexOf(targetEl);
          hvrImagesArray.forEach((img, idx) => {
            if (index == idx) {
              img.style.display = 'block';
            } else {
              img.style.display = 'none';
            }
          });
          hvr.querySelectorAll('.hvr__dot').forEach((dot, idx) => {
            if (index == idx) {
              dot.classList.add('hvr__dot--active');
            } else {
              dot.classList.remove('hvr__dot--active');
            }
          });
        };
        hvrSectors.addEventListener('mouseover', function (e) {
          if (e.target.matches('.hvr__sector')) {
            setActiveEl(e.target);
          }
        });
        hvrSectors.addEventListener('touchmove', function (e) {
          const position = e.changedTouches[0];
          const target = document.elementFromPoint(position.clientX, position.clientY);
          if (target.matches('.hvr__sector')) {
            setActiveEl(target);
          }
        });
      }
    });
  }
}

Код для инициализации скрипта:

new HvrSlider('.images');

Результат точно такой же, как и в примере выше:

See the Pen
Слайдер-превью изображений на JavaScript
by Dimox (@dimox)
on CodePen.

Комментарии (18)

  1. Владимир
    12 июня 2021 г. в 14:15

    Добрый день. В данном слайдере есть один недостаток. При просмотре с телефона страницу можно скролить вверх и вниз только касаясь мимо превью которые используют данный слайдер. На самом превью можно только листать картинки влево, вправо. В общем создаются неудобства при прокрутке страницы вверх, вниз когда пытаешься прокручивать хватаясь за превьюшку. Можно как то поправить данную проблему, что бы скролить страницу вверх, вниз можно было как и раньше касаясь любого места экрана?

    1. Да, есть такая проблема. Надо будет заняться поиском решения.

      1. Владимир
        21 июня 2021 г. в 15:25 / ответ на коммент Dimox

        Обнаружил еще одну проблему. В Firefox все картинки показываются в одну строку. В слайдер они превращаются только после того как по ним поводить курсором мышки. После этого все работает как слайдер. После перезагрузки страницы картинки снова в строку.

        1. Странно, у меня в Firefox всё работает, как положено.

    2. Проблема решена. Достаточно было удалить строку:

      e.preventDefault();
      1. Владимир
        23 июня 2021 г. в 13:47 / ответ на коммент Dimox

        Спасибо. Работает. В Firefox ваш оригинальный вариант и правда работает. Я в CSS вносил некоторые изменения что бы адаптировать к шаблону сайта, вот бяка и вылезла. В хром, опере, едж, везде нормально, а в Firefox бяка.

        1. Владимир
          23 июня 2021 г. в 13:51 / ответ на коммент Владимир

          изменения вот такие
          .image {
          display: block; max-width: 450px; width: 100%; height: auto;

          1. Владимир
            23 июня 2021 г. в 16:09 / ответ на коммент Владимир

            Это для того что бы блок слайдера был адаптивным. Как побороть это в Firefox не знаю.

            1. Нужно добавить такие стили:

              img {
                display: none;
              }
              
              img:first-child {
                display: block;
              }

              То есть по умолчанию показывать только первое изображение.

    3. Василий
      27 января 2023 г. в 00:42 / ответ на коммент Владимир
        let $hvr = $('<div>', { class: 'hvr' })
          .append(
            $('<div>', { class: 'hvr__images' })
              .append($('<div>', { class: 'hvr__sectors' })),
            $('<div>', { class: 'hvr__dots' })
          )
          .insertAfter($el)
          .prepend($el);
      
        let $hvrImages = $('.hvr__images', $hvr);
        let $hvrSectors = $('.hvr__sectors', $hvr);
        let $hvrDots = $('.hvr__dots', $hvr);
      
        $imgs.each(function() {
          $hvrSectors.prepend('<div class="hvr__sector"></div>');
          $hvrDots.append('<div class="hvr__dot"></div>');
        });
      
        $($imgs[0]).show();
        $($hvrDots.find('.hvr__dot')[0]).addClass('hvr__dot--active');
      
        let setActiveEl = function(el) {
          $imgs.hide().eq(el.index()).show();
          $hvrDots.find('.hvr__dot').removeClass('hvr__dot--active').eq(el.index()).addClass('hvr__dot--active');
        };
      
        $hvrSectors.on('mouseover', '.hvr__sector', function() {
          setActiveEl($(this));
        });
        
        $hvrSectors.on('touchmove', function(e) {
          let position = e.originalEvent.changedTouches[0];
          let target = document.elementFromPoint(position.clientX, position.clientY);
          if ($(target).is('.hvr__sector')) {
            setActiveEl($(target));
          }
        });
        
        // Добавленный код
        
        let $hvrPreview = $('.hvr__preview', $hvr);
        let startX = 0;
        let startY = 0;
      
  2. Владимир
    29 июня 2021 г. в 20:36

    Спасибо. Сработало после небольшой доработки, так как на странице есть и другие картинки и в итоге они пропадали а в слайдере отображались. Вот это вариант работает в моем случае корректно:
    .images img {
    display: none;
    }

    .images img:first-child {
    display: block;
    }

  3. Евгений
    5 июля 2021 г. в 09:58

    Переключение слайдов тапом работает очень коряво.
    В идеале должно работать так: https://codepen.io/i-did/pen/yLYRvVL

  4. андрей
    14 июня 2022 г. в 09:44

    А как на JS сделать плавность сметы этих изображений?

  5. Игнат
    15 июня 2022 г. в 13:36

    Спасибо за код. Подскажите, как сделать, что бы автоматически показывался первый слайд, если убрать мышь?

    1. Василий
      27 января 2023 г. в 00:48 / ответ на коммент Игнат
      let currentSlide = 0;
      let slideInterval = setInterval(nextSlide, 3000);
      
      function nextSlide() {
          currentSlide = (currentSlide + 1) % $imgs.length;
      
  6. Сергей
    20 октября 2022 г. в 15:07

    Добрый день. А как такой превью сделать для категории товаров в woocomerce

  7. Антон
    20 октября 2023 г. в 01:55

    Установил на сайт и возникла следующая проблема.
    При загрузке страницы отображаются все фото товара, а уже после того как навожу курсор, фото скрываются и все отображается как и должно.

    Как это исправить?

    1. 20 октября 2023 г. в 16:49 / ответ на коммент Антон

      Скройте стилями все изображения, кроме первого. В статье есть пример.

Ваш комментарий

Жирный текст

Ссылка

Цитата

Внутристрочный код

CSS-код

HTML-код

JavaScript-код

PHP-код