Набросал свой скрипт, конечно нужно дорабатывать, но как основа пойдет
стили и разметку думаю не сложно понять
<div class="home-cases__slider home-cases__stack" style="height: 675px;">
<div class="home-cases__slide ">
<div class="home-cases__slider-item ">
карточка
</div>
</div>
<div class="home-cases__slide ">
<div class="home-cases__slider-item ">
карточка
</div>
</div>
<div class="home-cases__slide ">
<div class="home-cases__slider-item ">
карточка
</div>
</div>
</div>
<script>
(function () {
function initStack(root) {
const stack = root.querySelector('.home-cases__stack');
if (!stack) return;
const slides = Array.from(stack.querySelectorAll('.home-cases__slide'));
if (!slides.length) return;
const scope = root.closest('.home-cases') || document;
const prevBt = scope.querySelector('.home-cases__slider-prev');
const nextBt = scope.querySelector('.home-cases__slider-next');
const surface = root.querySelector('.home-cases__viewport') || stack;
let i = 0;
function a11y() {
slides.forEach((sl) => {
const hidden = !sl.classList.contains('is-active') &&
!sl.classList.contains('is-prev-1') &&
!sl.classList.contains('is-prev-2');
sl.setAttribute('aria-hidden', hidden ? 'true' : 'false');
sl.querySelectorAll('a,button,input,select,textarea,[tabindex]')
.forEach(el => el.tabIndex = hidden ? -1 : 0);
});
}
function setHeight() {
stack.style.height = slides[i].offsetHeight + 'px';
}
function render() {
const n = slides.length;
const n1 = (i + 1) % n;
const n2 = (i + 2) % n;
slides.forEach((sl, idx) => {
sl.classList.remove('is-active','is-prev-1','is-prev-2','is-hidden');
if (idx === i) sl.classList.add('is-active');
else if (idx === n1) sl.classList.add('is-prev-1');
else if (n > 2 && idx === n2) sl.classList.add('is-prev-2');
else sl.classList.add('is-hidden');
});
setHeight();
a11y();
// якщо використовуємо ResizeObserver — перевішуємо на новий активний
ro && (ro.disconnect(), ro.observe(slides[i]));
}
// висота по зміні контенту — без «стрибків»
const ro = new ResizeObserver(setHeight);
ro.observe(slides[i]);
// кнопки
nextBt && nextBt.addEventListener('click', () => { i = (i + 1) % slides.length; render(); });
prevBt && prevBt.addEventListener('click', () => { i = (i - 1 + slides.length) % slides.length; render(); });
// === свайп / drag ===
(function attachSwipe() {
const HAS_POINTER = 'PointerEvent' in window;
const THRESHOLD = 40; // пікселів для спрацьовування
const TOLERANCE = 10; // мертва зона
let startX = 0, startY = 0;
let isDown = false, isDragging = false, isScrolling = false;
let swiped = false;
// не починати drag із клікабельних елементів
function isInteractive(el) {
return !!el.closest('a,button,input,select,textarea,label,[data-no-drag]');
}
function getPoint(e){
if (e.touches && e.touches[0]) return { x: e.touches[0].clientX, y: e.touches[0].clientY };
return { x: e.clientX, y: e.clientY };
}
function down(e){
if (isInteractive(e.target)) return; // даємо клікнути
const p = getPoint(e);
startX = p.x; startY = p.y;
isDown = true; isDragging = false; isScrolling = false;
surface.classList.add('is-grabbing');
// pointer capture (де є)
if (e.pointerId != null && surface.setPointerCapture) {
try { surface.setPointerCapture(e.pointerId); } catch(_) {}
}
}
function move(e){
if (!isDown) return;
const p = getPoint(e);
const dx = p.x - startX;
const dy = p.y - startY;
if (!isDragging) {
if (Math.abs(dx) < TOLERANCE && Math.abs(dy) < TOLERANCE) return;
if (Math.abs(dy) > Math.abs(dx)) { // вертикальний жест => скрол
isScrolling = true;
up(e);
return;
}
isDragging = true;
}
if (isDragging) {
// блокуємо нативний горизонтальний скрол під час свайпу
if (e.cancelable) e.preventDefault();
}
}
function up(e){
if (!isDown) return;
surface.classList.remove('is-grabbing');
if (!isScrolling && isDragging) {
const p = getPoint(e);
const dx = p.x - startX;
if (Math.abs(dx) > THRESHOLD) {
if (dx < 0) i = (i + 1) % slides.length; // ліворуч => наступний
else i = (i - 1 + slides.length) % slides.length; // праворуч => попередній
render();
swiped = true;
}
}
isDown = isDragging = isScrolling = false;
}
// блокуємо кліки після свайпу
root.addEventListener('click', function(e){
if (swiped) { e.preventDefault(); e.stopPropagation(); swiped = false; }
}, true);
// Події з урахуванням підтримки PointerEvent
if (HAS_POINTER) {
surface.addEventListener('pointerdown', down);
surface.addEventListener('pointermove', move, { passive: false });
surface.addEventListener('pointerup', up);
surface.addEventListener('pointercancel', up);
surface.addEventListener('pointerleave', up);
} else {
// fallback для старого iOS Safari
surface.addEventListener('touchstart', down, { passive: true });
surface.addEventListener('touchmove', move, { passive: false });
surface.addEventListener('touchend', up);
surface.addEventListener('mousedown', down);
window.addEventListener('mousemove', move);
window.addEventListener('mouseup', up);
}
})();
window.addEventListener('resize', setHeight, { passive: true });
render();
}
document.querySelectorAll('.home-cases').forEach(initStack);
})();
</script>