Uniwersalny skrypt karuzeli Slick.js w Divi – automatyczna obsługa wielu instancji na jednej stronie

Aleksandra Głaz
21 sierpnia, 2026

W poprzednim wpisie „Karuzela opinii w Divi zgodna z WCAG” opisałam wdrożenie karuzeli na module Notka (Blurb) i bibliotece Slick.js, scopowanej do jednej, konkretnej klasy wrappera. Zwróciłam tam uwagę na istotne ograniczenie: jeśli na tej samej podstronie potrzebne są dwie różne karuzele (np. opinii i zespołu), rozwiązanie wymaga albo dwóch osobnych bloków skryptu, albo jednej, zgeneralizowanej wersji obsługującej dowolną liczbę instancji automatycznie.

Ten wpis prezentuje właśnie tę drugą opcję — gotowy, uniwersalny skrypt, który wykrywa wszystkie karuzele na stronie i inicjalizuje każdą z nich niezależnie, bez konieczności kopiowania i dostosowywania kodu przy każdej kolejnej karuzeli.

Założenie: jedna wspólna klasa zamiast wielu unikalnych

W poprzednim wdrożeniu każda karuzela wymagała własnej, unikalnej klasy wrappera konsekwentnie powtórzonej w CSS i w selektorze JS. To rozwiązanie działa poprawnie, ale przy większej liczbie karuzel na wielu podstronach oznacza powtarzanie tego samego bloku kodu z inną nazwą klasy za każdym razem.

Zgeneralizowana wersja odwraca to podejście: zamiast unikalnej klasy dla każdej karuzeli, wszystkie karuzele na stronie otrzymują jedną, wspólną klasę-znacznik, np. slick-carousel-auto. Skrypt JS przechodzi po wszystkich elementach z tą klasą (.each()) i inicjalizuje każdy niezależnie, jako osobną instancję Slick.js — z własnym stanem odtwarzania, własnym przyciskiem pauzy i własną obsługą klasy .is-center.

Jak to działa w praktyce

Kluczowa różnica względem poprzedniej wersji polega na tym, że zmienna $carousel nie wskazuje już jednej, z góry określonej kolumny, tylko jest ustalana osobno dla każdej iteracji pętli .each(). Dzięki temu logika inicjalizacji, obsługa zdarzeń Slick.js i przycisk pauzy są za każdym razem powiązane wyłącznie z tą jedną, konkretną karuzelą, a nie z całą stroną.

Dodatkowo skrypt obsługuje opcjonalny atrybut data-carousel-label na wrapperze, który pozwala nadać każdej karuzeli własny, opisowy aria-label przycisku pauzy (np. „karuzeli opinii” lub „karuzeli zespołu”) bez modyfikowania samego kodu JS.

Struktura w Divi

  1. Wiersz (Row): każda karuzela na stronie otrzymuje tę samą klasę CSS slick-carousel-auto. Opcjonalnie można dodać atrybut data-carousel-label bezpośrednio w polu CSS ID & Classes w Divi (zakładka Zaawansowane → Atrybuty niestandardowe, jeśli wersja Divi na to pozwala) lub ustawić go ręcznie w kodzie po wdrożeniu.
  2. Kolumna: jedna kolumna wewnątrz wiersza, tak jak poprzednio.
  3. Moduły Notka (Blurb): dowolna liczba, każdy z klasą CSS slick-slide-item.
  4. Style wizualne (opcjonalnie): jeśli różne karuzele mają wyglądać inaczej (np. inny kolor tła kart), dodaj do wrappera dodatkową, drugą klasę obok slick-carousel-auto (np. karuzela-zespolu) i dopisz dla niej osobne, dodatkowe reguły CSS — sam mechanizm karuzeli i dostępności pozostaje wspólny dla wszystkich instancji.

Demo — dwie niezależne karuzele na jednej stronie

Poniżej dwie karuzele zainicjalizowane tym samym, wspólnym skryptem. Zatrzymanie jednej nie wpływa na działanie drugiej — to główny efekt, który ten wariant kodu ma zagwarantować.

Kompletny kod

Kod JS

<!-- Biblioteka Slick.js (pomiń, jeśli ładujesz ją już globalnie) -->
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/slick-carousel@1.8.1/slick/slick.min.js"></script>

<script type="text/javascript">
jQuery(function($) {
    setTimeout(function() {

        // Inicjalizacja każdej karuzeli ze wspólną klasą, niezależnie od siebie
        $('.slick-carousel-auto').each(function() {
            var $wrapper = $(this);
            var $carousel = $wrapper.find('.et_pb_column:has(.slick-slide-item)');

            if (!$carousel.length || $carousel.hasClass('slick-initialized')) return;

            var label = $wrapper.data('carousel-label') || 'karuzeli';
            var prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

            $carousel.on('init reInit afterChange', function(event, slick, currentSlide) {
                var idx = slick.currentSlide || 0;
                slick.$slides.removeClass('is-center');
                slick.$slides.eq(idx).addClass('is-center');
            });

            $carousel.slick({
                arrows: false,
                dots: true,
                infinite: true,
                speed: prefersReducedMotion ? 0 : 300,
                centerMode: true,
                centerPadding: '0px',
                slidesToShow: 3,
                slidesToScroll: 1,
                autoplay: !prefersReducedMotion,
                autoplaySpeed: 3000,
                pauseOnHover: true,
                pauseOnFocus: true,
                variableWidth: false,
                responsive: [
                    {
                        breakpoint: 980,
                        settings: {
                            slidesToShow: 2,
                            centerMode: false
                        }
                    },
                    {
                        breakpoint: 600,
                        settings: {
                            slidesToShow: 1,
                            centerMode: true,
                            centerPadding: '0px'
                        }
                    }
                ]
            });

            setTimeout(function() {
                try { $carousel.slick('setPosition'); } catch (e) {}
            }, 150);

            var $dotsContainer = $carousel.find('.slick-dots');
            if ($dotsContainer.length) {
                $dotsContainer.before(
                    '<button class="slick-pause-btn" aria-label="Zatrzymaj ' + label + '" tabindex="0"></button>'
                );
            }

            $carousel.parent().find('.slick-pause-btn').on('click', function() {
                if ($(this).hasClass('paused')) {
                    $carousel.slick('slickPlay');
                    $(this).removeClass('paused').attr('aria-label', 'Zatrzymaj ' + label);
                } else {
                    $carousel.slick('slickPause');
                    $(this).addClass('paused').attr('aria-label', 'Wznów ' + label);
                }
            });
        });

        // Jeden wspólny handler resize dla wszystkich zainicjalizowanych karuzel
        $(window).on('resize orientationchange', function() {
            $('.slick-carousel-auto .et_pb_column.slick-initialized').each(function() {
                try { $(this).slick('setPosition'); } catch (e) {}
            });
        });

    }, 100);
});
</script>

Kod CSS

/* Wspólny mechanizm dla wszystkich karuzel ze wspólną klasą */
.slick-carousel-auto .et_pb_column {
    padding-top: 40px !important;
    padding-bottom: 30px !important;
}

.slick-list {
    margin: 0 !important;
    padding: 0 !important;
}

.slick-slide {
    padding: 0 15px;
    box-sizing: border-box;
}

.slick-slide-item.et_pb_blurb .et_pb_blurb_content {
    background: #fff;
    padding: 20px;
    border-radius: 6px;
    box-shadow: 0px 4px 12px rgba(0,0,0,0.12);
    margin: 5px;
}

.slick-slide-item {
    transition: transform 0.55s ease, box-shadow 0.45s ease, opacity 0.45s ease;
    will-change: transform;
}

.slick-slide.is-center .slick-slide-item {
    transform: scale(1.25);
    opacity: 1;
}

@media (min-width: 601px) and (max-width: 980px) {
    .slick-slide {
        padding: 0 10px;
    }
    .slick-slide.is-center .slick-slide-item {
        transform: none !important;
        scale: 1 !important;
    }
}

@media (max-width: 600px) {
    .slick-list {
        margin-left: 0 !important;
        margin-right: 0 !important;
    }
    .slick-track {
        padding-left: 0 !important;
        padding-right: 0 !important;
    }
    .slick-slide {
        padding: 0 8px !important;
    }
    .slick-slide.is-center .slick-slide-item {
        transform: scale(1.02) !important;
    }
}

/* Kropki nawigacyjne - dostępność (WCAG) */
.slick-carousel-auto .slick-dots li {
    width: auto !important;
    height: auto !important;
    margin: 0 2px !important;
}

.slick-carousel-auto .slick-dots li button {
    width: 40px !important;
    height: 40px !important;
    padding: 0 !important;
    display: flex;
    align-items: center;
    justify-content: center;
}

.slick-carousel-auto .slick-dots li button:before {
    position: relative !important;
    font-size: 12px !important;
    line-height: 12px !important;
    width: 12px !important;
    height: 12px !important;
}

.slick-carousel-auto .slick-dots li.slick-active button:before {
    font-size: 14px !important;
}

.slick-carousel-auto .slick-dots li button:focus-visible {
    outline: 2px solid #0056b3;
    outline-offset: 2px;
    border-radius: 50%;
}

/* Przycisk pauzy - dostępność (WCAG) */
.slick-pause-btn {
    position: absolute;
    bottom: 0;
    right: 0;
    z-index: 10;
    background: none;
    border: none;
    cursor: pointer;
    font-size: 20px;
    line-height: 1;
    color: #333;
    transition: color 0.3s;
    padding: 10px 15px;
    min-width: 44px;
    min-height: 44px;
}

.slick-pause-btn:not(.paused)::after { content: "||"; }
.slick-pause-btn.paused::after { content: "►"; }

.slick-pause-btn:focus-visible {
    outline: 2px solid #0056b3;
    outline-offset: 2px;
    border-radius: 4px;
}

@media (max-width: 767px) {
    .slick-pause-btn {
        position: relative !important;
        float: right;
        margin-top: 3px;
    }
    .slick-dots {
        transform: translateY(-15px) !important;
    }
}

@media (prefers-reduced-motion: reduce) {
    .slick-slide-item,
    .slick-slide.is-center .slick-slide-item {
        transition: none !important;
        transform: none !important;
    }
}

Kiedy wybrać ten wariant, a kiedy poprzedni

Wariant z unikalną klasą dla pojedynczej karuzeli (opisany w poprzednim wpisie) pozostaje uzasadniony, gdy na stronie funkcjonuje tylko jedna karuzela lub gdy każda ma na tyle odmienną konfigurację (inna liczba slajdów, inne zachowanie na urządzeniach mobilnych), że wspólny mechanizm nie miałby zastosowania. Wariant zgeneralizowany, opisany w tym wpisie, sprawdza się przy wielu karuzelach o zbliżonej konfiguracji na jednej stronie lub w projektach, w których ten sam komponent ma być odtwarzany wielokrotnie na różnych podstronach bez każdorazowej modyfikacji kodu.

Checklista wdrożeniowa

  • Każdy wiersz z karuzelą ma nadaną wspólną klasę slick-carousel-auto.
  • Moduły Notka w każdej karuzeli mają klasę slick-slide-item.
  • Skrypt inicjalizuje karuzele przez .each(), bez sztywno zakodowanej, pojedynczej klasy wrappera.
  • Każda karuzela ma własny, niezależny przycisk pauzy i własny aria-label.
  • Zatrzymanie jednej karuzeli nie wpływa na działanie pozostałych na tej samej stronie.
  • Obsługa prefers-reduced-motion i resize działa jednolicie dla wszystkich instancji.

FAQ

Czy ten wariant nadaje się też do jednej karuzeli na stronie?

Tak — mechanizm .each() obsłuży poprawnie także pojedynczą karuzelę, więc ten skrypt można stosować jako uniwersalny standard, niezależnie od tego, ile instancji finalnie pojawi się na danej podstronie.

Czy różne karuzele mogą mieć różny wygląd przy tej samej klasie mechanizmu?

Tak. Klasa slick-carousel-auto odpowiada wyłącznie za mechanikę (inicjalizację, dostępność, responsywność). Wygląd kart można różnicować, dodając do wrappera dodatkową, drugą klasę i osobne reguły CSS dla tej klasy.

Co się stanie, jeśli dwie karuzele będą miały tę samą wartość data-carousel-label?

Nic złego pod względem funkcjonalnym — każda karuzela nadal działa niezależnie. Warto jednak nadawać unikalne opisy, żeby użytkownicy czytników ekranu mogli jednoznacznie rozróżnić, którą karuzelę aktualnie zatrzymują.


Potrzebujesz wdrożyć podobną karuzelę u siebie? Napisz do mnie i umów konsultację – sprawdzę kod i wdrożę rozwiązanie dopasowane do Twojej strony.