Karuzela opinii klientów to jeden z najczęściej wykorzystywanych elementów na stronach firmowych i instytucjonalnych, a jednocześnie jeden z trudniejszych do wdrożenia poprawnie pod względem dostępności. Standardowy moduł Slider w Divi ma ograniczoną kontrolę nad układem, a gotowe wtyczki dodają zwykle więcej kodu i skryptów, niż wymaga tego sama funkcjonalność. W tym wpisie pokazuję, jak zbudować karuzelę opinii (lub zespołu, referencji, dowolnych „kart”) na module Notka (Blurb) i bibliotece Slick.js, w pełnej zgodności z WCAG. Opisuję też jeden błąd wykryty podczas własnego wdrożenia oraz sposób jego naprawy.
Dlaczego nie natywny slider Divi
Natywny moduł Slider w Divi sprawdza się do prostych karuzeli zdjęć, ale przy karuzeli „kart” (opinia + zdjęcie + imię i nazwisko) szybko odsłania ograniczenia:
- brak trybu „center mode” z powiększeniem aktywnej karty,
- sztywna, trudna do dostosowania responsywność (3 → 2 → 1 slajdy to nie jest domyślne zachowanie),
- brak wbudowanej kontrolki pauzy wymaganej przez WCAG dla treści, które przesuwają się automatycznie.
Rozwiązaniem jest zbudowanie karuzeli na modułach Notka (Blurb), które renderują dowolną treść (zdjęcie, cytat, nazwisko), i napędzenie ich biblioteką Slick.js — sprawdzoną, lekką bibliotekę do karuzeli z dobrym wsparciem dla klawiatury.
Kluczowe rozwiązania niestandardowe
A. Naprawa ucinanych cieni. Slick.js domyślnie ustawia overflow: hidden na kontenerze, co ucina cień karty i nakładanie się sąsiednich elementów na powiększoną kartę środkową. Rozwiązanie to dodanie pionowego bufora (padding-top/bottom) w kontenerze kolumny Divi — bez ruszania z-index, co mogłoby wywołać efekt domina w innych elementach strony.
B. Własna klasa .is-center zamiast natywnej .slick-center. Natywna klasa Slick.js potrafi zachować się nieprzewidywalnie przy przejściach między liczbą widocznych slajdów (3 → 2 → 1). Nasłuchiwacz zdarzeń (init, reInit, afterChange) w JS dynamicznie przypisuje własną klasę do aktywnego slajdu, dając pełną kontrolę nad tym, który element CSS ma powiększyć.
C. Wymuszony reset pozycji. Divi ładuje swoje skrypty asynchronicznie, więc inicjalizacja Slick.js „za wcześnie” skutkuje błędnymi szerokościami toru karuzeli. setTimeout + wymuszone slick('setPosition') eliminują efekt obciętych kart przy pierwszym załadowaniu strony.
Nadawanie unikalnej klasy przy kilku karuzelach na stronie
Jest to szczegół łatwy do pominięcia na etapie wdrożenia, który może ujawnić się dopiero po dodaniu kolejnego elementu do strony.
Cały mechanizm — CSS i JS — musi być spięty jedną, unikalną klasą nadaną wierszowi (Row) w Divi, np. .karuzela-opinii-wrapper. Jeśli na tej samej podstronie powstanie druga karuzela zbudowana tą samą metodą (np. karuzela zespołu obok karuzeli opinii), a selektor w JS nie będzie ograniczony do właściwego wrappera, skrypt potraktuje obie kolumny jako jedną kolekcję. W praktyce oznacza to, że przycisk pauzy przypisany do jednej karuzeli zatrzyma również drugą, mimo że funkcjonalnie nie powinny być ze sobą powiązane.
Zasada jest prosta: każda karuzela na stronie powinna mieć własną, unikalną klasę wrappera, konsekwentnie użytą zarówno w CSS, jak i w selektorze jQuery w JS. Przy dwóch różnych karuzelach (.karuzela-opinii-wrapper i .karuzela-zespolu-wrapper) potrzebne są też dwa oddzielne bloki skryptu — po jednym na każdą klasę — albo jeden, bardziej uniwersalny skrypt, który iteruje po wszystkich wystąpieniach i inicjalizuje każde osobno. Ten drugi wariant wymaga innej struktury kodu (pętla .each() zamiast pojedynczego selektora) i sprawdza się przy większej liczbie karuzel na wielu podstronach.
Dostępność (WCAG) w praktyce
- Obszary dotykowe (Touch Target, WCAG 2.5.5). Kropki nawigacyjne wyglądają na 12–14px, ale ich fizyczny, klikalny obszar jest rozszerzony do 40×40px. Przycisk pauzy ma minimum 44×44px.
- Zatrzymanie ruchu (WCAG 2.2.2 Pause, Stop, Hide). Widoczny przycisk pauzy/play, dodatkowo
pauseOnHoveripauseOnFocus— karuzela zatrzymuje się też, gdy użytkownik nawiguje po niej klawiaturą. - Redukcja ruchu (
prefers-reduced-motion). Jeśli użytkownik ma w systemie włączoną redukcję animacji, karuzela ładuje się bez autoodtwarzania i bez płynnego przewijania — zakodowane równolegle w JS (wyłączenieautoplay,speed: 0) i w CSS (wyłączenietransition/transform). - Fokus klawiatury. Wyraźny, widoczny obrys
:focus-visiblena kropkach nawigacyjnych i przycisku pauzy.
Demo — zobacz działanie w praktyce
Poniżej uproszczone demo (poglądowe) pokazujące ten sam mechanizm: karta środkowa jest powiększona, kropki nawigują między slajdami, a przycisk zatrzymuje automatyczne przewijanie zgodnie z WCAG 2.2.2.
Struktura w Divi
- Wiersz (Row): nadaj klasę CSS
karuzela-opinii-wrapper. - Kolumna: jedna kolumna wewnątrz wiersza.
- Moduły Notka (Blurb): dowolna liczba, każdy z klasą CSS
slick-slide-item. Wyłącz natywne cienie i marginesy modułu w ustawieniach Divi, żeby nie kolidowały z CSS.
Gotowy 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() {
// Scoped do tej jednej karuzeli - jeśli dodajesz drugą, użyj innej klasy wrappera i osobnego bloku
var $carousel = $('.karuzela-opinii-wrapper .et_pb_column:has(.slick-slide-item)');
if (!$carousel.length || $carousel.hasClass('slick-initialized')) return;
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);
$(window).on('resize orientationchange', function() {
if ($carousel.hasClass('slick-initialized')) {
try { $carousel.slick('setPosition'); } catch(e){}
}
});
var $dotsContainer = $carousel.find('.slick-dots');
if ($dotsContainer.length) {
$dotsContainer.before('<button class="slick-pause-btn" aria-label="Zatrzymaj karuzelę opinii" 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 karuzelę opinii');
} else {
$carousel.slick('slickPause');
$(this).addClass('paused').attr('aria-label', 'Wznów karuzelę opinii');
}
});
}, 100);
});
</script>
Kod CSS
/* PODSTAWOWA STRUKTURA I BUFOR DLA CIENIA */
.karuzela-opinii-wrapper .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;
}
/* Powiększenie - tylko desktop, 3 slajdy */
.slick-slide.is-center .slick-slide-item {
transform: scale(1.25);
opacity: 1;
}
/* Tablet: 2 slajdy - bez powiększania */
@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;
}
}
/* Mobile: 1 slajd */
@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) */
.karuzela-opinii-wrapper .slick-dots li {
width: auto !important;
height: auto !important;
margin: 0 2px !important;
}
.karuzela-opinii-wrapper .slick-dots li button {
width: 40px !important;
height: 40px !important;
padding: 0 !important;
display: flex;
align-items: center;
justify-content: center;
}
.karuzela-opinii-wrapper .slick-dots li button:before {
position: relative !important;
font-size: 12px !important;
line-height: 12px !important;
width: 12px !important;
height: 12px !important;
}
.karuzela-opinii-wrapper .slick-dots li.slick-active button:before {
font-size: 14px !important;
}
.karuzela-opinii-wrapper .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;
}
}
/* Redukcja ruchu - dostępność (WCAG) */
@media (prefers-reduced-motion: reduce) {
.slick-slide-item,
.slick-slide.is-center .slick-slide-item {
transition: none !important;
transform: none !important;
}
}
Checklista dostępności do tego typu karuzeli
- Przycisk pauzy widoczny i działający, z aktualizującym się
aria-label. pauseOnHoveripauseOnFocuswłączone — karuzela zatrzymuje się, gdy użytkownik najeżdża na nią kursorem lub nawiguje klawiaturą.- Klikalny obszar kropek nawigacyjnych minimum 40×40px, przycisku pauzy minimum 44×44px.
- Skrypt reaguje na
prefers-reduced-motion— brak autoplay i płynnego przewijania. - Wyraźny obrys
:focus-visiblena wszystkich elementach nawigacyjnych. - Każda karuzela na stronie ma własną, unikalną klasę wrappera — zarówno w CSS, jak i w selektorze JS.
FAQ
Tak, ale każda musi mieć własną, unikalną klasę wrappera (np. .karuzela-opinii-wrapper i .karuzela-zespolu-wrapper) użytą konsekwentnie w CSS i w selektorze JS — inaczej przycisk pauzy jednej karuzeli zatrzyma też drugą.
Sama biblioteka to niewielki plik JS + CSS, ładowany raz na stronę (nawet przy kilku karuzelach z tą samą biblioteką). Realny koszt wydajności zwykle bierze się nie z biblioteki, a z liczby i wagi zdjęć w kartach.
Nie musi — w tym wdrożeniu tablet i mobile celowo rezygnują z mocnego powiększenia (albo je minimalizują), bo na wąskim ekranie efekt powiększenia częściej przeszkadza w czytaniu treści, niż pomaga.
Potrzebujesz wdrożyć podobną karuzelę u siebie? Napisz do mnie i umów konsultację – sprawdzę kod i wdrożę rozwiązanie dopasowane do Twojej strony.
