Skip to content

Сценарий «Истории»

Сценарий «истории» можно собрать полностью на событиях Tvist без внешних таймеров:

  • autoplayProgress — заполняет активный сегмент 0..1
  • longPressStart / longPressEnd — пауза/возобновление по удержанию
  • waitForVideo: true — для HTML <video> переход по окончанию ролика
  • reachEnd — сигнал закрыть модалку или переключить внешний контейнер

Интерактивный пример

Stories

Вложенные слайдеры групп и историй с сегментным прогрессом

Маша
Утро у моря
Маша
Кофе-брейк
Маша
Рабочий спринт
Саша
Скейт-парк
Саша
Город вечером
Ира
Горы
Ира
Треккинг
Ира
Закат

Desktop: группы переключаются без анимации. Mobile: группы через cube-эффект.

Пример
<div class="tvist-v1 stories-groups">
  <div class="tvist-v1__container">
    <div class="tvist-v1__slide stories-group-slide">
      <div class="stories-inner-wrap">
        <div class="stories-progress">
          <div class="stories-progress__segment"><div class="stories-progress__fill" data-progress-segment="0"></div></div>
          <div class="stories-progress__segment"><div class="stories-progress__fill" data-progress-segment="1"></div></div>
          <div class="stories-progress__segment"><div class="stories-progress__fill" data-progress-segment="2"></div></div>
        </div>
        <div class="tvist-v1 stories-inner">
          <div class="tvist-v1__container">
            <div class="tvist-v1__slide stories-inner-slide">Story 1</div>
            <div class="tvist-v1__slide stories-inner-slide">Story 2</div>
            <div class="tvist-v1__slide stories-inner-slide">Story 3</div>
          </div>
        </div>
      </div>
    </div>

    <div class="tvist-v1__slide stories-group-slide">
      <div class="stories-inner-wrap">
        <div class="stories-progress">
          <div class="stories-progress__segment"><div class="stories-progress__fill" data-progress-segment="0"></div></div>
          <div class="stories-progress__segment"><div class="stories-progress__fill" data-progress-segment="1"></div></div>
        </div>
        <div class="tvist-v1 stories-inner">
          <div class="tvist-v1__container">
            <div class="tvist-v1__slide stories-inner-slide">Story A</div>
            <div class="tvist-v1__slide stories-inner-slide">Story B</div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

<script type="module">
  import { Tvist } from 'tvist'

  const MOBILE_BREAKPOINT = 767
  const groupRoot = document.querySelector('.stories-groups')
  const innerRoots = groupRoot.querySelectorAll('.stories-inner')
  const progressRoots = groupRoot.querySelectorAll('.stories-progress')

  // Внешний слайдер групп:
  // desktop = slide (speed 0), mobile = cube (speed 540)
  const groupSlider = new Tvist(groupRoot, {
    perPage: 1,
    gap: 0,
    loop: false,
    drag: true,
    effect: 'slide',
    speed: 0,
    breakpointsBase: 'window',
    breakpoints: {
      [MOBILE_BREAKPOINT]: {
        effect: 'cube',
        speed: 540,
        cubeEffect: {
          slideShadows: false,
          shadow: false,
          shadowOffset: 0,
          shadowScale: 1
        }
      }
    }
  })

  // Вложенные слайдеры историй внутри каждой группы
  const innerSliders = Array.from(innerRoots).map((root) =>
    new Tvist(root, {
      perPage: 1,
      gap: 0,
      loop: false,
      drag: true,
      holdToPause: true,
      autoplay: {
        delay: 2800,
        pauseOnHover: false,
        waitForVideo: false
      },
      on: {
        autoplayProgress: ({ progress, index }) => {
          const fillEls = root.parentElement.querySelectorAll('[data-progress-segment]')
          fillEls.forEach((el, segmentIndex) => {
            if (segmentIndex < index) el.style.width = '100%'
            else if (segmentIndex > index) el.style.width = '0%'
            else el.style.width = `${Math.max(0, Math.min(progress * 100, 100))}%`
          })
        },
        slideChangeStart: (index) => {
          const fillEls = root.parentElement.querySelectorAll('[data-progress-segment]')
          fillEls.forEach((el, segmentIndex) => {
            el.style.width = segmentIndex < index ? '100%' : '0%'
          })
        }
      }
    })
  )

  // Важно для cube: прогресс находится внутри каждой грани.
  // Поэтому при вращении куба индикатор "приклеен" к текущему слайду.
</script>

Базовая конфигурация

js
const slider = new Tvist('.tvist-v1', {
  holdToPause: {
    enabled: true,
    threshold: 100,
    root: 'slider',
    exclude: '[data-tvist-no-hold]',
    cancelOnDrag: true,
  },
  autoplay: {
    delay: 5000,        // используется для не-видео слайдов
    waitForVideo: true, // видео ждём до конца
  },
  video: {
    autoplay: true,
    muted: true,
    pauseOnHold: true,
  },
  on: {
    autoplayProgress: ({ progress, index }) => {
      renderStorySegments(index, progress)
    },
    reachEnd: () => {
      closeStoriesModal()
    },
  },
})

Сегменты прогресса

js
const segments = [...document.querySelectorAll('.story-segment')]

function renderStorySegments(activeIndex, activeProgress) {
  segments.forEach((el, i) => {
    if (i < activeIndex) {
      el.style.transform = 'scaleX(1)'
      return
    }
    if (i > activeIndex) {
      el.style.transform = 'scaleX(0)'
      return
    }
    el.style.transform = `scaleX(${activeProgress})`
  })
}

Зоны тапа «назад / вперёд»

Внешние зоны можно оставить вне root слайдера:

js
document.querySelector('[data-story-prev]')?.addEventListener('click', () => {
  slider.prev()
})

document.querySelector('[data-story-next]')?.addEventListener('click', () => {
  slider.next()
})

Если в этих элементах не нужно удержание, добавьте data-tvist-no-hold и укажите exclude в holdToPause.

Вложенные слайдеры

Рекомендуемый паттерн:

  • внешний слайдер групп: autoplay: false
  • внутренний слайдер медиа: autoplay + holdToPause + waitForVideo

Это упрощает синхронизацию и исключает конфликт таймеров между уровнями. Для hold на внутреннем слайдере pointerdown не всплывает к родителю; дополнительно на слайде можно слушать DOM CustomEvent из TVIST_DOM_EVENTS (события API).

Если поверх карточки лежат свои зоны «назад / вперёд», они перехватывают касания — удержание для паузы сработает только там, где событие доходит до root внутреннего слайдера (например центральная полоса, если боковые заняты оверлеями).