Slider

W poniższym tekście postaramy się zrobić dynamiczny slider. Nie byle jaki. Całkowicie profesjonalny slider.

HTML i CSS

Zacznijmy od przygotowania odpowiedniego stylowania, oraz struktury HTML dla naszego slidera:

Pokaż HTML

<div class="slider" id="mySlider">
    <div class="slider-slides">
        <article class="slider-slide is-active">
            <h2 class="slider-slide-title">
                1 Lorem ipsum dolor sit.
            </h2>
            <div class="slider-slide-text">
                Lorem ipsum dolor sit amet, consectetur adipisicing elit. Mollitia dolore inventore, ipsam optio. Magni repellat officia autem fuga, laboriosam facilis?
            </div>
        </article>
        <article class="slider-slide">
            <h2 class="slider-slide-title">
                2 Lorem ipsum dolor sit.
            </h2>
            <div class="slider-slide-text">
                Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ipsum exercitationem asperiores perspiciatis sapiente ducimus atque dicta labore dolores sequi ut.
            </div>
        </article>
        <article class="slider-slide">
            <h2 class="slider-slide-title">
                3 Lorem ipsum dolor sit.
            </h2>
            <div class="slider-slide-text">
                Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repellendus id quas, quia consectetur repellat quis! Minus voluptatibus, dolores iure deserunt.
            </div>
        </article>
    </div>
</div>

Pokaż CSS

.slider {
    position: relative;
    background: #eee;
    background-size: cover;
    background-position: center center;
    display: grid;
    grid-template-columns: 4rem 1fr 4rem;
    grid-template-rows: 1fr;
    height: 350px;
    background-size: cover;
    background-position: center center;
    max-width: 900px;
    border-radius: 10px;
    box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
    margin: 40px auto;
    overflow: hidden;
}
.slider-slides {
    grid-column: 1 / -1;
    grid-row: 1 / -1;
    display: grid;
    grid-template-columns: 1fr;
    grid-template-rows: 1fr;
}
.slider-slide {
    grid-column: 1 / 2;
    grid-row: 1 / 2;
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
    opacity:0;
    z-index: 0;
}
.slider-slide.is-active {
    opacity: 1;
    z-index: 1;
}

/* animacja */
.slider-slide {
    opacity: 0;
    transition:0.4s all;
}
.slider-slide.is-active {
    opacity: 1;
}

/* prev / next */
.slider-nav {
    grid-column: 1 / -1;
    grid-row: 1 / -1;
    display: grid;
    grid-template-columns: 4rem 1fr 4rem;
    grid-template-rows: 1fr;
}
.slider-button-prev,
.slider-button-next {
    cursor: pointer;
    z-index: 2;
    background: rgba(0,0,0,0.1);
    border:0;
    text-indent:-999px;
    overflow:hidden;
    position: relative;
    z-index: 1;
    background-size: 30px;
    background-repeat: no-repeat;
    background-position: center;
    text-indent: -9999px;
    overflow: hidden;
}
.slider-button-prev {
    grid-column: 1 / 2;
    grid-row: 1 / 2;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-chevron-left' viewBox='0 0 16 16'%3E  %3Cpath fill-rule='evenodd' d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'%3E%3C/path%3E%3C/svg%3E");
}
.slider-button-next {
    grid-column: -1 / -2;
    grid-row: 1 / 2;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-chevron-right' viewBox='0 0 16 16'%3E  %3Cpath fill-rule='evenodd' d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'%3E%3C/path%3E%3C/svg%3E");
}
.slider-button-prev:focus,
.slider-button-next:focus {
    outline: none;
}

/* stronicowanie */
.slider-pagination {
    z-index: 2;
    position: absolute;
    left:0;
    bottom:1rem;
    width:100%;
    display: flex;
    justify-content: center;
}

.slider-pagination-button {
    background: transparent;
    width:2rem;
    height:2rem;
    border:0;
    text-indent:-999px;
    overflow: hidden;
    cursor: pointer;
    position: relative;
}
.slider-pagination-button:before {
    content:'';
    position: absolute;
    left:50%;
    top:50%;
    transform:translate(-50%, -50%);
    background: transparent;
    width:0.9rem;
    height:0.9rem;
    border-radius: 50%;
    transition:0.5s all;
    box-shadow:inset 0 0 0 1px rgba(255,255,255,0.5);
}
.slider-pagination-button.is-active:before {
    background: #fff;
}
.slider-pagination-button:focus {
    outline: none;
}

/* wnętrze slajdów */
.slider-slide-title {
    color:#fff;
    font-family: sans-serif;
    position: relative;
    left:-3rem;
    opacity:0;
    font-size:2.3rem;
    font-weight: bold;
    text-transform: uppercase;
    margin-top:0;
    text-align: center;
    font-family: 'Roboto Condensed', sans-serif;
}
.slider-slide-text {
    color:#fff;
    position: relative;
    left:-3rem;
    opacity:0;
    font-size:1.2rem;
    color:rgba(255,255,255,.9);
    padding-left:4rem;
    padding-right:4rem;
    text-align: center;
    font-family: serif;
    font-style: italic;
    max-width:60%;
}
.slider .slider-slide-title {
    left:-3rem;
    opacity:0;
    transition: 2s left, 2s opacity;
}
.slider .slider-slide-text {
    left:-3rem;
    opacity:0;
    transition: 2s 1s left, 2s 1s opacity;
}
.slider-slide.is-active .slider-slide-title {
    left:0;
    opacity:1;
}
.slider-slide.is-active .slider-slide-text {
    left:0;
    opacity:1;
}

klasa Slider

Zakładamy, że sliderów na stronie możemy mieć kilka, więc przy jego tworzeniu dobrze jest skorzystać z klasy.


class Slider {
    constructor(query) {
        this.slider = document.querySelector(query); //element slider
        this.slides = [...this.slider.querySelectorAll(".slider-slide")];
        this.currentSlide = Math.max(0, this.slides.findIndex(el => el.classList.contains("is-active"))); //aktualny slide
    }
}

const slider = new Slider("#mySlider");

Na początku wyłapujemy ze strony slider oraz jego slajdy. Do ustawienia indexu aktualnie aktywnego slajdu wykorzystaliśmy Math.max(). Za pomocą metody findIndex() zwracam slajd, który posiada klasę is-active. Może się zdarzyć, że początkowo żaden ze slajdów nie będzie miał takiej klasy. Metoda findIndex zwróci nam wtedy -1. Korzystając z Math.max(0, -1) zwrócimy wtedy pierwszy slajd i to właśnie jego ustawimy na aktywny. Metodę findIndex możemy użyć dla this.slides tylko dlatego, że wcześniej zamieniliśmy tą kolekcję na tablicę za pomocą spread operator.

Metoda createPrevNext()

Pierwszą z metod będzie createPrevNext(), która stworzy przyciski do przełączania slajdów.


class Slider {
    constructor() {
        ...
        this.createPrevNext();
    }

    createPrevNext() {
        this.btnPrev = document.createElement("button");
        this.btnPrev.type = "button";
        this.btnPrev.innerText = "Poprzedni slajd";
        this.btnPrev.classList.add("slider-button", "slider-button-prev");
        this.btnPrev.addEventListener("click", this.slidePrev.bind(this));

        this.btnNext = document.createElement("button");
        this.btnNext.type = "button";
        this.btnNext.innerText = "Następny slajd"
        this.btnNext.classList.add("slider-button", "slider-button-next");
        this.btnNext.addEventListener("click", this.slideNext.bind(this));

        const nav = document.createElement("div");
        nav.classList.add("slider-nav");
        this.slider.append(this.btnPrev);
        this.slider.append(this.btnNext);
        this.slider.append(nav);
    }
}

Przypisując metody do przycisków musieliśmy użyć metody bind(). Wynika to z tego, że gdy używasz klasycznych funkcji (ale i gdy podpinasz do zdarzeń funkcje przez ich nazwę) w ich wnętrzu this wskazuje na "kliknięty" element, a nie obiekt naszej klasy. Za pomocą bind lub funkcji strzałkowej możemy to zmienić:


this.btnPrev.addEventListener("click", this.slidePrev.bind(this));

//lub

this.btnPrev.addEventListener("click", () => this.slidePrev());

Powyższą metodę odpalmy na końcu konstruktora.

Metody slidePrev() i slideNext()

W powyższej metodzie po kliknięciu na przyciski odpalamy zmianę aktualnego slajdu na poprzedni/następny za pomocą metod slidePrev() i slideNext():


class Slider {
    ...

    slidePrev() {
        this.currentSlide--;
        if (this.currentSlide < 0) {
            this.currentSlide = this.slides.length - 1;
        }
        this.setSlide(this.currentSlide);
    }

    slideNext() {
        this.currentSlide++;
        if (this.currentSlide > this.slides.length - 1) {
            this.currentSlide = 0;
        }
        this.setSlide(this.currentSlide);
    }
}

Metody tylko wyliczają odpowiedni index slajdu. Do samej zmiany slajdu posłuży nam oddzielna metoda.

Metoda setSlide()

Przełączanie slajdu polegać będzie na usunięciu klasy is-active z aktywnego slajdu, a następnie ustawieniu jej następnemu slajdowi:


class Slider {
    constructor() {
        ...
        this.setSlide(this.currentSlide);
    }

    setSlide(index) {
        //usuwamy klasę is-active wszystkim slajdom
        this.slides.forEach(slide => {
            slide.classList.remove("is-active");
        });

        //dodajemy ją tylko wybranemu
        this.slides[index].classList.add("is-active");

        //aktualny slide przestawiamy na wybrany
        this.currentSlide = index;
    }
}

Przy okazji odpalmy tą metodę w konstruktorze (linia 4), dzięki czemu po uruchomieniu slajdera ustawi on sobie automatycznie odpowiedni slajd na aktywny.

Metoda createPagination()

Zmiany slajdów mamy kompletne. Kolejną czynnością jest stworzenie metody generującej paginację zmieniającą slajdy:


class Slider {
    constructor() {
        ...
        this.createPagination();
    }

    createPagination() {
        this.dots = document.createElement("div");
        this.dots.classList.add("slider-pagination");

        //tworzymy pętlę w ilości liczby slajdów
        this.slides.forEach((el, i) => {
            const btn = document.createElement("button");
            btn.classList.add("slider-pagination-button");
            btn.type = "button";
            btn.innerText = i + 1;
            btn.addEventListener("click", () => this.setSlide(i));
            this.dots.append(btn);
        });

        this.slider.append(this.dots);
    }
}

Tak samo jak poprzednio odpalmy ją też od razu w konstruktorze.

Przy zmianie slajdu powinniśmy ustawić na aktywną odpowiednią kropkę paginacji. Stwórzmy do tego oddzielną metodę:


class Slider {
    ...

    setSlide(index) {
        this.slides.forEach(slide => {
            slide.classList.remove("is-active");
        });

        this.slides[index].classList.add("is-active");
        const dots = [...this.dots.children];
        dots.forEach(dot => dot.classList.remove("is-active"));
        dots[index].classList.add("is-active");
        this.currentSlide = index;
    }

    ...
}

Automatyzacja

Dodajmy do naszego slidera automatyczne przełączanie slajdów. Wykorzystamy tutaj setTimeout(). Wystarczy za pomocą tej funkcji wywołać powyższą metodę slideNext().


class Slider {
    constructor() {
        ...
        this.time = null;
        ...
    }
    ...

    setSlide(index) {
        ...

        clearTimeout(this.time);
        this.time = setTimeout(() => this.slideNext(), 6000); //co 6 sekund automatycznie się przełączy
    }
}

Gdy użytkownik najedzie kursorem na slider, możemy założyć, że może chce się dokładniej przyjrzeć, albo na spokojnie przeczytać co tam się znajduje (ale przecież wiemy, że tak na prawdę chce się delektować naszym dziełem). Na ten czas przydało by się wyłączyć automatyczne przewijanie. Posłuży nam do tego nowa metoda, którą odpalimy na końcu konstruktora:


class Slider {
    constructor() {
        ...
        this.handleMouseEnterAndOut();
    }

    handleMouseEnterAndOut() {
        this.slider.addEventListener("mouseenter", () => clearTimeout(this.time));

        this.slider.addEventListener("mouseout", () => {
            clearTimeout(this.time);
            this.time = setTimeout(() => this.slideNext(), 6000);
        })
    }
}

Opcje

W powyższym kodzie przełączanie slajdów jest ustawione na sztywno co 6 sekund. Aż prosi się, by przy wywołaniu naszego slidera programista mógł ustawić takie wartości.

Zastosujemy tutaj podejście, które dokładniej opisałem tutaj. Do naszej klasy trafi obiekt z opcjami, które ustawi programista. Opcje te nadpiszą domyślne ustawienia, w wyniku czego dostaniemy nowy scalony obiekt z ustawieniami slidera.


//to co sobie ustawi programista
const opts = {
    width : 666
}

//domyślne ustawienia
const defaultOpts = {
    width: 10,
    height: 200
}

//scalony obiekt
console.log({ ...defaultOptions, ...opts }); //{width: 666, height:200}

lub


const opts = {
    width : 666
}

const defaultOptions = {
    width: 10,
    height: 200
}

console.log(Object.assign({}, defaultOptions, opts)); //{width: 666, height:200}

Dodajmy domyślne ustawienia w konstruktorze:


class Slider {
    constructor(query, opts) {
        this.slider = document.querySelector(query); //element slider
        this.slides = [...this.slider.querySelectorAll(".slider-slide")];
        this.time = null; //tutaj będziemy podczepiać setTimeout
        this.currentSlide = Math.max(0, this.slides.findIndex(el => el.classList.contains("is-active"))); //aktualny slide

        const defaultOpts = {
            pauseTime: 0
        };
        this.options = {...defaultOpts, ...opts};

        this.createPrevNext();
        this.createPagination();
        this.setSlide(this.currentSlide);
    }

    ...
}

Domyślnie czas przerwy między kolejnym przełączaniem będzie wynosił 0. Jeżeli programista tego nie zmieni, nie będziemy przełączać automatycznie slajdów:


class Slider {
    constructor() {
        this.slider = document.querySelector(query); //element slider
        this.slides = [...this.slider.querySelectorAll(".slider-slide")];
        this.time = null; //tutaj będziemy podczepiać setTimeout
        this.currentSlide = Math.max(0, this.slides.findIndex(el => el.classList.contains("is-active"))); //aktualny slide

        const defaultOpts = {
            pauseTime: 0
        };
        this.options = {...defaultOpts, ...opts};
        this.autoChangeSlides = typeof this.options.pauseTime === "number" && this.options.pauseTime > 0;
        this.createPrevNext();
        this.createPagination();
        if (this.autoChangeSlides) this.handleMouseEnter();
        this.setSlide(this.currentSlide);
    }

    setSlide(index) {
        this.slides.forEach(slide => {
            slide.classList.remove("is-active");
        });

        this.slides[index].classList.add("is-active");

        const dots = [...this.dots.children];
        dots.forEach(dot => dot.classList.remove("is-active"));
        dots[index].classList.add("is-active");

        this.currentSlide = index;

        if (this.autoChangeSlides) {
            clearTimeout(this.time);
            this.time = setTimeout(() => this.slideNext(), this.options.pauseTime)
        }
    }

    ...
}

Podobne opcje możemy też ustawić dla innych rzeczy - np. generowania przycisków poprzedni/następny czy też paginacji:


class Slider {
    constructor(query, opts) {
        this.slider = document.querySelector(query); //element slider
        this.slides = [...this.slider.querySelectorAll(".slider-slide")];
        this.time = null;
        this.currentSlide = Math.max(0, this.slides.findIndex(el => el.classList.contains("is-active")));

        const defaultOpts = {
            pauseTime: 0,
            generateDots: true,
            generatePrevNext: true
        };
        this.options = {...defaultOpts, ...opts};
        this.autoChangeSlides = typeof this.options.pauseTime === "number" && this.options.pauseTime > 0;
        if (this.options.generatePrevNext) this.createPrevNext();
        if (this.options.generateDots) this.createPagination();
        if (this.autoChangeSlides) {
            this.handleMouseEnter();
        }
        this.setSlide(this.currentSlide);
    }

    slidePrev() {
        this.currentSlide--;
        if (this.currentSlide < 0) {
            this.currentSlide = this.slides.length - 1;
        }
        this.setSlide(this.currentSlide);
    }

    slideNext() {
        this.currentSlide++;
        if (this.currentSlide > this.slides.length - 1) {
            this.currentSlide = 0;
        }
        this.setSlide(this.currentSlide);
    }

    setSlide(index) {
        this.slides.forEach(slide => {
            slide.classList.remove("is-active");
        });

        this.slides[index].classList.add("is-active");

        if (this.options.generateDots) {
            const dots = [...this.dots.children];
            dots.forEach(dot => dot.classList.remove("is-active"));
            dots[index].classList.add("is-active");
        }

        this.currentSlide = index;

        if (this.autoChangeSlides) {
            clearTimeout(this.time);
            this.time = setTimeout(() => this.slideNext(), this.options.pauseTime)
        }
    }

    createPrevNext() {
        this.btnPrev = document.createElement("button");
        this.btnPrev.type = "button";
        this.btnPrev.innerText = "Poprzedni slajd";
        this.btnPrev.classList.add("slider-button", "slider-button-prev");
        this.btnPrev.addEventListener("click", this.slidePrev.bind(this));

        this.btnNext = document.createElement("button");
        this.btnNext.type = "button";
        this.btnNext.innerText = "Następny slajd"
        this.btnNext.classList.add("slider-button", "slider-button-next");
        this.btnNext.addEventListener("click", this.slideNext.bind(this));

        const nav = document.createElement("div");
        nav.classList.add("slider-nav");
        this.slider.append(this.btnPrev);
        this.slider.append(this.btnNext);
        this.slider.append(nav);
    }

    createPagination() {
        this.dots = document.createElement("div");
        this.dots.classList.add("slider-pagination");

        this.slides.forEach((el, i) => {
            const btn = document.createElement("button");
            btn.classList.add("slider-pagination-button");
            btn.type = "button";
            btn.innerText = i + 1;
            btn.addEventListener("click", () => this.setSlide(i));
            this.dots.append(btn);
        });

        this.slider.append(this.dots);
    }

    handleMouseEnter() {
        this.slider.addEventListener("mouseenter", () => clearTimeout(this.time));

        this.slider.addEventListener("mouseout", () => {
            if (this.autoChangeSlides) {
                this.time = setTimeout(() => this.slideNext(), this.options.pauseTime)
            }
        })
    }
}

Demo

Aby użyć naszego slidera, skorzystamy z kodu:


//automatyczny slider bez paginacji i strzałek
const options1 = {
    pauseTime : 3000,
}
const slider1 = new Slider("#slider1", options1);

//manualny slider z przyciskami
const options2 = {
    pauseTime : 0,
    generatePrevNext : false,
    generatePagination : false
}
const slide2 = new Slider("#slider2", options2);
Demo

Trening czyni mistrza

Jeżeli chcesz sobie potrenować zdobytą wiedzę, zadania znajdują się w repozytorium pod adresem: https://github.com/kartofelek007/zadania

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem.
Aha - i ta strona korzysta z ciasteczek.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.