chevron-left chevron-right

[CSS] Jak utworzyć animowany przycisk typu toggle button za pomocą CSS?

Po dłuższym okresie braku aktywności na blogu powracam z nowym wpisem. Dzisiejszy wpis będzie dotyczył utworzenia dwustanowego przycisku, typu toggle button, którego stan będzie można zmienić za pomocą CSS, bez użycia JavaScript.

Kod HTML

Aby móc wykorzystać omawiany przykład przycisku typu toggle musimy przygotować odpowiednią strukturę kodu HTML, która wygląda następująco:

<label class="btn-toggle">
    <input class="btn-toggle__checkbox" type="checkbox"/>
    <span class="btn-toggle__body">
        <span class="btn-toggle__body__switch"></span>
        <span class="btn-toggle__body__track">
            <span class="btn-toggle__body__track__option--view">View</span>
            <span class="btn-toggle__body__track__option--edit">Edit</span>
        </span>
    </span>
</label>

Jak można zauważyć, wykorzystałem elementy <span/> do określenia suwaka i tła pod suwakiem. Jest to związane z tym, że chciałem zachować poprawną strukturę kodu HTML, gdzie wewnątrz elementów liniowych, takich jak <label/> nie powinny się znajdować elementy blokowe (np. takie jak <div/>) tylko elementy liniowe, m.in. <span/>.

Kod CSS

Mając gotowy kod HTML, możemy zabrać się do stylowania tego kodu. Za pomocą poniższego kodu określimy jak ma się zachowywać przycisk w określonych sytuacjach:

.btn-toggle {
    position: relative;
    z-index: 1;
    display: inline-block;
    height: 30px;
    cursor: pointer;
}
 
.btn-toggle__checkbox {
    position: absolute;
    /*
        umieszczamy nasz checkbox poza zasiegiem kursora,
        tj. pod tekstem i tłem suwaka. Dzięki temu użytkownik
        nie będzie go widział, ale dalej będzie w stanie go kliknąć.
     */
    opacity: 0;
    z-index: -1;
    visibility: hidden;
}
 
.btn-toggle__body {
    width: 100px;
    height: 30px;
    background: #fff;
    border: 1px solid #dadde1;
    display: inline-block;
    position: relative;
    z-index: 1;
    border-radius: 50px;
}
 
.btn-toggle__body__switch {
    width: 30px;
    height: 30px;
    display: inline-block;
    position: absolute;
    z-index: 2;
    left: 0;
    top: 50%;
    border-radius: 50%;
    border: 1px solid #fff;
    box-shadow: 0 2px 2px rgba(0, 0, 0, 0.13);
    color: #fff;
    background: #439fd8;
    transition:
        transform cubic-bezier(0.34, 1.61, 0.7, 1) .25s,
        background cubic-bezier(0.34, 1.61, 0.7, 1) .25s;
    transform: translate3d(-1px, -50%, 0);
}
 
.btn-toggle__body__switch:before {
    content: '«';
    display: block;
    line-height: 30px;
    font-size: 20px;
    text-align: center;
}
 
.btn-toggle__body__track {
    position: absolute;
    z-index: 1;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    overflow: hidden;
    border-radius: 50px;
}
 
[class*="btn-toggle__body__track__option"] {
    position: absolute;
    z-index: 1;
    top: 0;
    bottom: 0;
    width: 100px;
    text-align: center;
    line-height: 30px;
    transition:
        transform cubic-bezier(0.34, 1.61, 0.7, 1) .25s,
        background cubic-bezier(0.34, 1.61, 0.7, 1) .25s;
}
 
.btn-toggle__body__track__option--view {
    transform: translate3d(10%, 0, 0);
    background: #fff;
    color: #439fd8;
}
 
.btn-toggle__body__track__option--edit {
    transform: translate3d(-100%, 0, 0);
    background: #439fd8;
    color: #fff;
}
 
.btn-toggle:hover .btn-toggle__body__switch {
    border-color: #b5bbc3;
    transform: scale(1.06) translate3d(-1px, -47%, 0);
}
 
.btn-toggle:active .btn-toggle__body__switch {
    transform: scale(0.94) translate3d(-1px, -53%, 0);
}
 
/*
  gdy checkbox nie jest zaznaczony, ustawiamy mu style które pokażą informację o tym,
  że zmiana stanu przycisku zaznaczy checkbox
*/
.btn-toggle__checkbox:not(:checked) ~ .btn-toggle__body > .btn-toggle__body__switch {
    transform: translate3d(70px, -50%, 0);
    background: #fff;
    color: #439fd8;
    border: 1px solid #ccd0d6;
}
 
.btn-toggle__checkbox:not(:checked) ~ .btn-toggle__body > .btn-toggle__body__switch:before {
    content: '»';
}
 
.btn-toggle:hover .btn-toggle__checkbox:not(:checked) ~ .btn-toggle__body > .btn-toggle__body__switch {
    transform: scale(1.06) translate3d(66px, -47%, 0);
}
 
.btn-toggle:active .btn-toggle__checkbox:not(:checked) ~ .btn-toggle__body > .btn-toggle__body__switch {
    transform: scale(0.94) translate3d(74px, -53%, 0);
}
 
.btn-toggle__checkbox:not(:checked) ~ .btn-toggle__body .btn-toggle__body__track__option--view {
    transform: translate3d(110%, 0, 0);
}
 
.btn-toggle__checkbox:not(:checked) ~ .btn-toggle__body .btn-toggle__body__track__option--edit {
    transform: translate3d(-10%, 0, 0);
}

Powyższy kod CSS jest odpowiedzialny za ukrycie pola typu checkbox przed wzrokiem użytkownika strony. Ponadto, dodaje odpowiedni design i przejścia między stanami zaznaczenia pola typu checkbox. Dzięki temu, użytkownik ma przyjemną kontrolkę do używania na stronie lub w aplikacji internetowej.

Aby poprawić wydajność animacji przejścia wykorzystałem funkcję translate3d, dzięki czemu przeglądarka nie będzie musiała przeliczać ponownie położenia layoutu, tak jakby to miało miejsce w przypadku wykorzystania własności CSS: left, right.

Podsumowanie

Powyższy przykład nie zawiera wszystkich właściwości CSS z vendor prefiksami (tzn. z przedrostkami wymaganymi przez starsze wersje przeglądarek), po to aby kod był bardziej przejrzysty. Dobrym sposobem na uzupełnienie brakującego kodu z vendor prefiksami jest użycie Autoprefixera (można go uruchomić z linii poleceń w terminalu lub za pomocą Grunt.js).

Mam nadzieję, że zaprezentowany przeze mnie sposób na tworzenie efektownie wyglądających przycisków okaże się przydatny. Efekt był testowany na przeglądarkach Mozilla Firefox, Google Chrome oraz Opera.

źródło

  • Fajny. Muszę wypróbować. Nie będzie się chyba gryzł z bootstrapem?

  • Nie powinien się gryźć z bootstrapem. Style są napisane w metodologii BEM i odnoszą się tylko do toggle buttona 🙂

  • Efekt wizualnie jest bardzo fajny, ale pomyślałbym nad aspektami dostępności.

    Checkbox z natury jest przełącznikiem true/false. Albo się akceptuje regulamin, albo nie. Tak też jest zastosowane w oryginalnym demo. Ty natomiast rozszerzasz możliwości wyboru, wprowadzając dwa stany: „view” i „edit”, czego nie da się wyprowadzić z samego stanu zaznaczenia checkboxa (natomiast już całkiem normalne byłoby pytanie typu „czy pozwolić na edycję?”).

    Co więcej, brakuje jakiejś odgórnej etykietki dla tego switcha, żeby wiedzieć w zasadzie co możemy „view”/”edit”.

    Dlatego też widziałbym to raczej jako kontrolkę złożoną z dwóch input[type=radio], w której poszczególne opcje („view” i „edit”) byłyby ich labelami. Dzięki temu AT dostałoby jasny komunikat co jest do czego. Kod praktycznie przeszedł kilka małych zmian, a dostępność wzrosła 😉

    Proof of concept: http://bzdety.comandeer.pl/switch.html

    BTW z tego byłby fajny web component… Nie obrazisz się jak to zaadaptuję? 😉

  • Zgadzam się z tym co napisałeś odnośnie dostępności. Chodziło o pokazanie konceptu, który posiada odpowiednio przygotowane transformacje CSS.
    W przypadku Twojego konceptu coś nie zagrało, bo kliknięcie w kółko (switch) nie powoduje zmiany stanu przycisku.

    Nie obrażę się, możesz go zaadaptować na swoje potrzeby 🙂

  • Wiem, wiem – kółko nie działa. Za bardzo skupiłem się na dostępności 😉

  • A tak z innej beczki – ostrzeżenie o AdBlocku raczej nie powinno mówić o cookies… 😉

  • SpeX

    Ale kodując go z tekstami On/Off, Tak/Nie itp spokojnie może zastąpić również checkbox.

  • No tak, bo to są stany binarne, tworzące opozycję. Ale w wypadku edit/view taka zależność nie zachodzi i wówczas nie nadaje się to na normalnego checkboxa

  • Comandeer, w tym przypadku mogę się nie zgodzić. Patrząc na to z perspektywy edytora treści można mówić o stanie binarnym. Albo masz podgląd tekstu albo tryb edycji.

  • Ok, ale to już mówimy o dość szczególnym przypadku. Fakt, w przypadku edytora tekstu ten switch może być prezentacją stanu edycji (czy jest włączona czy nie), ale takie rozumowanie nie sprawdzi się we wszystkich możliwych przypadkach.

  • Tak, masz rację. Akurat element zaproponowany w tym artykule jest inspirowany tym czym się zajmuję na co dzień, czyli tworzeniem interfejsów oprogramowania do zarządzania treścią 🙂