Сценарий «Истории»
Сценарий «истории» можно собрать полностью на событиях Tvist без внешних таймеров:
autoplayProgress— заполняет активный сегмент0..1longPressStart/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>Базовая конфигурация
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()
},
},
})Сегменты прогресса
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 слайдера:
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 внутреннего слайдера (например центральная полоса, если боковые заняты оверлеями).