Как сделать цикличный слайдер из табов?

Всем доброго дня!

Есть семь табов, по каждому из них контент идет простым скриншотом. Задача: табы сделать цикличным (бесконечным) слайдером и чтобы активный таб всегда был по центру. Я пробовала на swiper, но то ли еще не до конца понимаю его особенности, то ли туплю. Loop свойство работает криво, табы не центруются, а вечно уезжают влево.

Собственно, пример блока, как надо сделать (на первом экране): clickup.com

Я уже два дня с этим бьюсь и даже не понимаю с чего начать и как лучше подойти к этой задаче. Поэтому прошу совета: какие инструменты использовать? Подходит ли вообще swiper для этой задачи? Читала, у него с loop часто проблемы случаются.

Заранее спасибо!


Ответы (1 шт):

Автор решения: Vladislav G.

Можно и на ванильке, например так. Правда там еще есть над чем подумать в плане оптимизации.

class TabSlider {
    constructor (templates) {
        this.elem = templates.tabSlider.cloneNode(true);
        this.tabContainer = this.elem.querySelector('.tab-container');
        this.contentContainer = this.elem.querySelector('.content-container');

        this.tTab = templates.tab;
        this.tImg = templates.contentImg;

        this.tabs = [];
        this.contents = [];
        this.selectedTabIndex = null;

        this.tabContainer.addEventListener('click', evt => {
            if (evt.target.classList.contains('tab') && !evt.target.classList.contains('tab-selected')) {
                const tab = evt.target;

                tab.scrollIntoView({
                    behavior: 'smooth',
                    inline: 'center'
                });

                setTimeout(() => {
                    this.changeTab(evt.target.appIndex);
                    this.changeContent(evt.target.appIndex);
                }, 400);
            }
        });

        new ResizeObserver(() => {
            if (this.selectedTabIndex === null) return;
            this.changeTab(this.selectedTabIndex);
        }).observe(this.tabContainer);
    }

    setData (data) {
        data.forEach(obj => {
            const tab = this.tTab.cloneNode(true);
            const img = this.tImg.cloneNode(true);

            tab.textContent = obj.name;
            img.src = obj.imgSrc;

            this.tabs.push(tab);
            this.contents.push(img);
        });

        this.changeTab(0);
        this.changeContent(0);
    }

    changeTab (appIndex) {
        this.selectedTabIndex = appIndex;

        const selectedTab = this.tabs[appIndex].cloneNode(true);
        selectedTab.classList.add('tab-selected');
        const tbs = [selectedTab];

        const stockCount = this.tabs.length + 1;
        let index = appIndex;
        for (let i = 1; i < stockCount; i++) {
            index++;
            if (index >= this.tabs.length) index = 0;
            const tab = this.tabs[index].cloneNode(true);
            tab.appIndex = index;
            tbs.push(tab);
        }

        index = appIndex;
        for (let i = stockCount - 2; i >= 0; i--) {
            index--;
            if (index < 0) index = this.tabs.length - 1;

            const tab = this.tabs[index].cloneNode(true);
            tab.appIndex = index;
            tbs.unshift(tab);
        }

        this.tabContainer.replaceChildren(...tbs);

        setTimeout(() => {
            selectedTab.scrollIntoView({
                inline: 'center'
            });
        });
    }

    changeContent (appIndex) {
        this.contentContainer.replaceChildren(this.contents[appIndex]);
    }

    render () {
        return this.elem;
    }
}

const data = [
    {
        name: 'tab 1',
        imgSrc: 'https://avatars.mds.yandex.net/i?id=ec714219a44b1338e90f1d70b7d52fa0_l-5233919-images-thumbs&n=13'
    },

    {
        name: 'tab 2',
        imgSrc: 'https://sun9-70.userapi.com/impf/c836232/v836232101/535eb/qJln4fQrF0c.jpg?size=1280x800&quality=96&sign=da46d0c434bc60cfebb78f4a135c01a8&c_uniq_tag=sKTXim4XymLflTB1CYcwPkyuTsT1pdrvaqmOTQnd0pA&type=album'
    },

    {
        name: 'tab 3',
        imgSrc: 'https://i.pinimg.com/originals/f0/d4/66/f0d4669bf8ac56992e8e69bc3744c1b1.jpg'
    },

    {
        name: 'tab 4',
        imgSrc: 'https://main-cdn.sbermegamarket.ru/big2/hlr-system/138/279/616/352/134/1/600011876299b0.jpeg'
    },

    {
        name: 'tab 5',
        imgSrc: 'https://avatars.mds.yandex.net/get-mpic/3986638/img_id1570444521473320416.jpeg/orig'
    },

    {
        name: 'tab 6',
        imgSrc: 'https://avatars.mds.yandex.net/get-mpic/11722550/2a0000018b4389164775c48cbec8c88dc526/orig'
    },

    {
        name: 'tab 7',
        imgSrc: 'https://zvetnoe.ru/upload/catalog/2019/10/MZ2730.jpg'
    },

];

(function () {
    const t = document.querySelector('#temp-slider').content;
    const tElems = {
        tabSlider: t.querySelector('.tab-slider'),
        tab: t.querySelector('.tab'),
        contentImg: t.querySelector('.content-img')
    };

    const slider = new TabSlider(tElems);
    slider.setData(data);

    document.querySelector('.main').append(slider.render());
})();
*,
*::before,
*::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

.page {
    margin: auto;
    background-color: #fff;
}

.page-title {
    margin: 20px 0;
    color: rgb(134, 27, 255);
    font-family: monospace;
    text-align: center;
}

.tab-slider {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 20px;

    max-width: 1024px;
    width: 80%;

    margin: auto;

    overflow: hidden;
}

.wrapper {
    display: grid;
    grid-template-columns: 1fr 60% 1fr;
    grid-template-rows: 1fr;
    width: 100%;
}

.shadow {
    pointer-events: none;
    z-index: 1;

    &._left {
        grid-row: 1/2;
        grid-column: 1/2;
        background: linear-gradient(90deg, rgba(255, 255, 255, 1.0) 10%, rgba(255, 255, 255, 0.8));
    }

    &._right {
        grid-row: 1/2;
        grid-column: 3/4;
        background: linear-gradient(270deg, rgba(255, 255, 255, 1.0) 10%, rgba(255, 255, 255, 0.8));
    }
}

.radial-shadow {
    grid-row: 1/2;
    grid-column: 2/3;

    width: 100%;
    height: 100%;

    background: radial-gradient(rgba(255, 255, 255, 0), rgba(255, 255, 255, 1));

    pointer-events: none;
    z-index: 1;

}

.tab-container {
    grid-row: 1/2;
    grid-column: 1/4;

    display: flex;
    flex-direction: row;
    gap: 60px;

    padding: 10px;

    height: 80px;

    overflow-x: scroll;

    &::-webkit-scrollbar {
        display: none;
    }
}

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

    height: 100%;
    aspect-ratio: 1.0;

    border-radius: 35%;
    background-color: #afa;

    user-select: none;
    cursor: pointer;

    transition: transform .2s linear;

    &:hover {
        transform: scale(1.08) translate(0, -4px);
    }
}

.tab-selected {
    background-color: rgb(245, 170, 255);
}

.content-img {
    display: block;
    width: 100%;
    aspect-ratio: 1.6;

    object-position: center;
    object-fit: cover;

    border-radius: 14px;
}
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Tab Slider</title>

    <link rel="stylesheet" href="index.css">
</head>

<body class="page">
    <h1 class="page-title">tab slider</h1>
    <main class="main"></main>

    <template id="temp-slider">
        <div class="tab-slider">
            <div class="wrapper">
                <div class="shadow _left"></div>
                <div class="tab-container"></div>
                <div class="radial-shadow"></div>
                <div class="shadow _right"></div>
            </div>
            <div class="content-container"></div>
        </div>

        <div class="tab"></div>

        <img class="content-img" src="" alt="">
    </template>

    <script src="index.js"></script>
</body>

</html>

→ Ссылка