Tworzenie komponentu przełącznika motywu

Podstawowy opis tworzenia komponentu przełącznika motywów, który jest dostosowany do potrzeb osób niepełnosprawnych.

W tym poście chcę podzielić się z Wami sposobem na tworzenie komponentu przełącznika ciemnego i jasnego motywu. Wypróbuj wersję demonstracyjną.

Demo zwiększa rozmiar przycisku, aby ułatwić jego widoczność

Jeśli wolisz film, oto wersja tego posta w YouTube:

Omówienie

Strona internetowa może udostępniać ustawienia umożliwiające kontrolowanie schematu kolorów zamiast polegania wyłącznie na ustawieniach systemu. Oznacza to, że użytkownicy mogą przeglądać treści w trybie innym niż ustawiony w ustawieniach systemu. Na przykład system użytkownika jest w jasnym motywie, ale użytkownik woli, aby witryna była wyświetlana w ciemnym motywie.

Podczas tworzenia tej funkcji należy wziąć pod uwagę kilka kwestii związanych z inżynierią internetową. Na przykład przeglądarka powinna jak najszybciej poinformować o ustawieniu, aby zapobiec miganiu kolorów na stronie. Element sterujący musi najpierw zsynchronizować się z systemem, a potem zezwolić na wyjątki zapisane po stronie klienta.

Diagram pokazuje podgląd zdarzeń interakcji z załadunkiem strony i dokumentem JavaScript, aby pokazać, że istnieją 4 ścieżki ustawiania motywu.

Znacznik

Do przełączania należy używać wartości <button>, ponieważ wtedy możesz korzystać z funkcji i zdarzeń interakcji udostępnianych przez przeglądarkę, takich jak zdarzenia kliknięcia i możliwość skupienia uwagi.

Przycisk

Przycisk musi mieć klasę do użycia w CSS i identyfikator do użycia w JavaScript. Ponieważ treść przycisku to ikona, a nie tekst, dodaj atrybut title, aby podać informacje o celu przycisku. Na koniec dodaj element [aria-label], aby przechowywać stan przycisku ikony, dzięki czemu czytniki ekranu będą mogły udostępniać stan motywu osobom niedowidzącym.

<button 
  class="theme-toggle" 
  id="theme-toggle" 
  title="Toggles light & dark" 
  aria-label="auto"
>
  …
</button>

aria-labelaria-live uprzejmie

Aby wskazać czytnikowi ekranu, że zmiany w przycisku aria-label powinny być odczytywane, dodaj do niego element aria-live="polite".

<button 
  class="theme-toggle" 
  id="theme-toggle" 
  title="Toggles light & dark" 
  aria-label="auto" 
  aria-live="polite"
>
  …
</button>

To rozszerzenie znaczników sygnalizuje czytnikom ekranu, że należy grzecznie poinformować użytkownika o zmianach, zamiast używać do tego celu tagu aria-live="assertive". W przypadku tego przycisku ogłosi „light” (jasny) lub „dark” (ciemny), w zależności od tego, co aria-label stało się.

Ikona skalowalnej grafiki wektorowej (SVG)

SVG umożliwia tworzenie wysokiej jakości skalowanych kształtów z minimalną ilością znaczników. Interakcja z przyciskiem może wywoływać nowe stany wizualne wektorów, dzięki czemu format SVG świetnie nadaje się do tworzenia ikon.

Ten znacznik SVG umieszczasz wewnątrz tagu <button>:

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  …
</svg>

aria-hidden został dodany do elementu SVG, aby czytniki ekranu wiedziały, że mają go ignorować, ponieważ jest oznaczony jako element prezentacyjny. Jest to przydatne w przypadku dekoracji wizualnych, takich jak ikona wewnątrz przycisku. Oprócz wymaganego atrybutu viewBox w elemencie dodaj wysokość i szerokość z podobnych powodów, dla których obrazy powinny mieć rozmiary w linii.

Słońce

Ikona słońca z wyblakłymi promieniami i różowa strzałka wskazująca na kółko pośrodku.

Grafika słońca składa się z koła i linii, które SVG ma wśród dostępnych kształtów. Element <circle> jest wyśrodkowany, ponieważ właściwości cxcy mają wartość 12, która jest połową rozmiaru widocznego obszaru (24), a następnie promień (r) ma wartość 6, która określa rozmiar.

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
</svg>

Dodatkowo właściwość maski wskazuje identyfikator elementu SVG, który utworzysz później, a na koniec kolor wypełnienia, który pasuje do koloru tekstu na stronie (currentColor).

promienie słoneczne,

Ikona słońca z wyblakłym środkiem i różowa strzałka wskazująca promienie słoneczne.

Następnie bezpośrednio pod okręgiem, wewnątrz grupy elementów <g>, dodano linie promieniowania słońca.

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
  <g class="sun-beams" stroke="currentColor">
    <line x1="12" y1="1" x2="12" y2="3" />
    <line x1="12" y1="21" x2="12" y2="23" />
    <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
    <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
    <line x1="1" y1="12" x2="3" y2="12" />
    <line x1="21" y1="12" x2="23" y2="12" />
    <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
    <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
  </g>
</svg>

Tym razem zamiast wartości fill currentColor ustawiono stroke dla każdej linii. Linie i koła tworzą słońce z promieniami.

Księżyc

Aby stworzyć iluzję płynnego przejścia między światłem (słońce) a ciemnością (księżyc), księżyc jest rozszerzeniem ikony słońca za pomocą maski SVG.

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
  <g class="sun-beams" stroke="currentColor">
    …
  </g>
  <mask class="moon" id="moon-mask">
    <rect x="0" y="0" width="100%" height="100%" fill="white" />
    <circle cx="24" cy="10" r="6" fill="black" />
  </mask>
</svg>
Grafika z 3 poziomymi warstwami, która pokazuje, jak działa maskowanie. Górna warstwa to biały kwadrat z czarnym okręgiem. Warstwa środkowa to ikona słońca.
Warstwę dolną oznaczono jako wynik i zawiera ona ikonę słońca z wycięciem w miejscu czarnego koła w warstwie górnej.

Maski w SVG są bardzo skuteczne, ponieważ umożliwiają usuwanie lub uwzględnianie części innej grafiki za pomocą kolorów białego i czarnego. Ikona słońca będzie zasłonięta przez kształt księżyca<circle>z maską SVG, po prostu przez przesuwanie koła w obszarze maski.

Co się stanie, jeśli nie wczytuje się plik CSS?

Zrzut ekranu przedstawiający zwykły przycisk przeglądarki z ikoną słońca.

Warto przetestować plik SVG tak, jakby nie wczytywał się CSS, aby upewnić się, że wynik nie jest zbyt duży ani nie powoduje problemów z układem. Atrybuty inline height i width w pliku SVG oraz użycie atrybutu currentColor zapewniają minimalne reguły stylów, których przeglądarka może użyć, jeśli kod CSS się nie załaduje. Dzięki temu można stosować skuteczne style obronne w przypadku zakłóceń w sieci.

Układ

Komponent przełącznika motywu zajmuje niewielką powierzchnię, więc do jego ułożenia nie musisz używać siatki ani flexboxa. Zamiast tego używane są pozycjonowanie SVG i przekształcenia CSS.

Style

.theme-toggle stylów

Element <button> to kontener na kształty i style ikon. Ten kontekst nadrzędny będzie zawierać kolory i rozmiary dostosowane do wyświetlania na różnych urządzeniach, które zostaną przekazane do pliku SVG.

Pierwszym zadaniem jest zastąpienie kwadratowego przycisku okrągłym i usunięcie domyślnych stylów przycisku:

.theme-toggle {
  --size: 2rem;
  
  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;
}

Następnie dodaj kilka stylów interakcji. Dodaj styl kursora dla użytkowników myszy. Dodaj touch-action: manipulation, aby uzyskać szybką reakcję na dotyk. Usuń półprzezroczyste podświetlenie, które iOS stosuje do przycisków. Na koniec daj obrysowi stanu skupienia trochę miejsca od krawędzi elementu:

.theme-toggle {
  --size: 2rem;

  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;

  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  outline-offset: 5px;
}

Plik SVG wewnątrz przycisku również wymaga zastosowania niektórych stylów. Plik SVG powinien pasować do rozmiaru przycisku, a jego krawędzie powinny być zaokrąglone, aby nadać mu łagodny wygląd:

.theme-toggle {
  --size: 2rem;

  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;

  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  outline-offset: 5px;

  & > svg {
    inline-size: 100%;
    block-size: 100%;
    stroke-linecap: round;
  }
}

Automatyczne dopasowywanie rozmiaru za pomocą zapytania o media hover

Rozmiar ikony przycisku (2rem) jest nieco za mały, co nie stanowi problemu dla użytkowników myszy, ale może być utrudnieniem w przypadku grubego wskaźnika, takiego jak palec. Dopasuj przycisk do wielu wytycznych dotyczących rozmiaru dotykowego, używając zapytania o media wyświetlane po najechaniu kursorem, aby określić zwiększenie rozmiaru.

.theme-toggle {
  --size: 2rem;
  
  
  @media (hover: none) {
    --size: 48px;
  }
}

Style SVG słońca i księżyca

Przycisk zawiera interaktywne aspekty komponentu przełącznika motywu, a SVG wewnątrz zawiera elementy wizualne i animowane. Tutaj możesz nadać ikonie piękny wygląd i ożywić ją.

Jasny motyw

ALT_TEXT_HERE

Aby animacje skalowania i obracania były wykonywane z poziomu środka kształtów SVG, ustaw ich transform-origin: center center. Kolory adaptacyjne udostępnione przez przycisk są używane przez kształty. Księżyc i słońce używają dostarczonego przycisku var(--icon-fill)var(--icon-fill-hover) do wypełnienia, a promienie słoneczne używają zmiennych do obrysu.

.sun-and-moon {
  & > :is(.moon, .sun, .sun-beams) {
    transform-origin: center center;
  }

  & > :is(.moon, .sun) {
    fill: var(--icon-fill);

    @nest .theme-toggle:is(:hover, :focus-visible) > & {
      fill: var(--icon-fill-hover);
    }
  }

  & > .sun-beams {
    stroke: var(--icon-fill);
    stroke-width: 2px;

    @nest .theme-toggle:is(:hover, :focus-visible) & {
      stroke: var(--icon-fill-hover);
    }
  }
}

Ciemny motyw

ALT_TEXT_HERE

W przypadku stylów księżyca należy usunąć promienie słoneczne, powiększyć okrąg słońca i przesunąć maskę koła.

.sun-and-moon {
  @nest [data-theme="dark"] & {
    & > .sun {
      transform: scale(1.75);
    }

    & > .sun-beams {
      opacity: 0;
    }

    & > .moon > circle {
      transform: translateX(-7px);

      @supports (cx: 1px) {
        transform: translateX(0);
        cx: 17px;
      }
    }
  }
}

Zwróć uwagę, że ciemny motyw nie zawiera żadnych zmian ani przejść kolorów. Kolory są przypisane do nadrzędnego komponentu przycisku, w którym są już dostosowane do ciemnego i jasnego tła. Informacje o przejściu powinny być dostępne po zapytaniu o media z preferencjami użytkownika.

Animacja

Przycisk powinien być funkcjonalny i mieć stan, ale na tym etapie bez przejść. W kolejnych sekcjach określasz jak i co ma się przenieść.

Udostępnianie zapytań o multimedia i importowanie łagodzeń

Aby ułatwić umieszczanie przejść i animacji w ramach preferencji użytkownika dotyczących animacji w systemie operacyjnym, wtyczka PostCSS Custom Media umożliwia korzystanie z składni specyfikacji CSS dla zmiennych definicji „media”:

@custom-media --motionOK (prefers-reduced-motion: no-preference);

/* usage example */
@media (--motionOK) {
  .sun {
    transition: transform .5s var(--ease-elastic-3);
  }
}

Aby uzyskać unikalne i łatwe w użyciu łagodnienia CSS, zaimportuj część easingsOpen Props:

@import "https://unpkg.com/open-props/easings.min.css";

/* usage example */
.sun {
  transition: transform .5s var(--ease-elastic-3);
}

Słońce

Przejścia słońca będą bardziej zabawne niż księżyca, a efekt ten uzyskasz dzięki użyciu łagodnego wygładzania. Promienie słoneczne powinny się lekko poruszać podczas obracania, a centrum słońca powinno się lekko poruszać podczas skalowania.

Style domyślne (jasny motyw) definiują przejścia, a style motywu ciemnego definiują dostosowywanie do motywu jasnego:

​​.sun-and-moon {
  @media (--motionOK) {
    & > .sun {
      transition: transform .5s var(--ease-elastic-3);
    }

    & > .sun-beams {
      transition: 
        transform .5s var(--ease-elastic-4),
        opacity .5s var(--ease-3)
      ;
    }

    @nest [data-theme="dark"] & {
      & > .sun {
        transform: scale(1.75);
        transition-timing-function: var(--ease-3);
        transition-duration: .25s;
      }

      & > .sun-beams {
        transform: rotateZ(-25deg);
        transition-duration: .15s;
      }
    }
  }
}

W panelu Animacja w Narzędziach deweloperskich w Chrome znajdziesz osi czasu z przejściami animacji. Można sprawdzić czas trwania całej animacji, jej elementów i czasu trwania łagodnego przejścia.

Przejście z jasnego na ciemny
Przejście z ciemnego na jasny

Księżyc

Pozycje światła i ciemności księżyca są już ustawione. Dodaj style przejścia w ramach zapytania o media --motionOK, aby wprowadzić je w życie, zachowując przy tym preferencje użytkownika dotyczące animacji.

Opóźnienie i czas trwania są kluczowe dla płynnego przejścia. Jeśli słońce zniknie zbyt wcześnie, przejście nie będzie wyglądać tak dobrze, jak mogłoby.

​​.sun-and-moon {
  @media (--motionOK) {
    & .moon > circle {
      transform: translateX(-7px);
      transition: transform .25s var(--ease-out-5);

      @supports (cx: 1px) {
        transform: translateX(0);
        cx: 17px;
        transition: cx .25s var(--ease-out-5);
      }
    }

    @nest [data-theme="dark"] & {
      & > .moon > circle {
        transition-delay: .25s;
        transition-duration: .5s;
      }
    }
  }
}
Przejście z jasnego na ciemny
Przejście z ciemnego na jasny

Preferuje ograniczenie animacji

Większość wyzwań dotyczących interfejsu użytkownika staram się uzupełnić o niektóre animacje, np. przejścia z przezroczystości, aby zaspokoić potrzeby użytkowników, którzy wolą ograniczone ruchy. Ten komponent lepiej jednak sprawdzał się w przypadku natychmiastowych zmian stanu.

JavaScript

W tym komponencie JavaScript ma dużo pracy, od zarządzania informacjami ARIA dla czytników ekranu po pobieranie i ustawianie wartości z miejscowego magazynu danych.

Wczytywanie strony

Ważne jest, aby podczas wczytywania strony nie występowało miganie kolorów. Jeśli użytkownik korzystający z ciemnego schematu kolorów wskaże, że woli jasne kolory w tym komponencie, a następnie przeładuje stronę, najpierw będzie ona ciemna, a potem zmieni się na jasną. Aby temu zapobiec, trzeba było uruchomić niewielką ilość blokującego kodu JavaScript z celem ustawienia atrybutu HTML data-theme jak najwcześniej.

<script src="./theme-toggle.js"></script>

W tym celu najpierw wczytuje się zwykły tag <script> w dokumencie <head>, zanim zostanie zastosowane jakiekolwiek oznaczenie CSS lub <body>. Gdy przeglądarka napotka taki niezaznaczony skrypt, uruchomi go przed resztą kodu HTML. Korzystając z tego momentu blokowania, możesz ustawić atrybut HTML, zanim główny plik CSS wypełni stronę, co zapobiegnie miganiu lub zmianie kolorów.

Kod JavaScript najpierw sprawdza preferencje użytkownika w pamięci lokalnej, a jeśli nic nie znajdzie, sprawdza preferencje systemowe:

const storageKey = 'theme-preference'

const getColorPreference = () => {
  if (localStorage.getItem(storageKey))
    return localStorage.getItem(storageKey)
  else
    return window.matchMedia('(prefers-color-scheme: dark)').matches
      ? 'dark'
      : 'light'
}

Następnie jest analizowana funkcja ustawiania preferencji użytkownika w pamięci lokalnej:

const setPreference = () => {
  localStorage.setItem(storageKey, theme.value)
  reflectPreference()
}

Następnie następuje funkcja modyfikująca dokument zgodnie z ustawieniami.

const reflectPreference = () => {
  document.firstElementChild
    .setAttribute('data-theme', theme.value)

  document
    .querySelector('#theme-toggle')
    ?.setAttribute('aria-label', theme.value)
}

Ważne jest, aby na tym etapie zwrócić uwagę na stan analizowania dokumentu HTML. Przeglądarka nie wie jeszcze o przycisku „#theme-toggle”, ponieważ tag <head> nie został całkowicie przeanalizowany. Jednak przeglądarka ma tag document.firstElementChild, czyli tag <html>. Funkcja próbuje ustawić oba tagi, aby zachować ich synchroniczność, ale przy pierwszym uruchomieniu może ustawić tylko tag HTML. Funkcja querySelector na początku niczego nie znajdzie, a opcjonalny operator łańcuchowania zapobiega błędom składni, gdy nie uda się znaleźć funkcji setAttribute i próbuje się ją wywołać.

Następnie wywoływana jest funkcja reflectPreference(), aby ustawić atrybut data-theme w dokumencie HTML:

reflectPreference()

Przycisk nadal potrzebuje atrybutu, więc zaczekaj na zdarzenie wczytania strony, a potem możesz bezpiecznie wysyłać zapytania, dodawać detektory i ustawiać atrybuty:

window.onload = () => {
  // set on load so screen readers can get the latest value on the button
  reflectPreference()

  // now this script can find and listen for clicks on the control
  document
    .querySelector('#theme-toggle')
    .addEventListener('click', onClick)
}

Przełączanie

Gdy klikniesz przycisk, motyw musi zostać zastąpiony w pamięci JavaScript i w dokumencie. Należy sprawdzić bieżącą wartość motywu i podjąć decyzję o jego nowym stanie. Po ustawieniu nowego stanu zapisz go i zaktualizuj dokument:

const onClick = () => {
  theme.value = theme.value === 'light'
    ? 'dark'
    : 'light'

  setPreference()
}

Synchronizacja z systemem

W przypadku tego przełącznika motywów występuje synchronizacja z ustawieniami systemu w miarę ich zmiany. Jeśli użytkownik zmieni ustawienia systemu, gdy strona i ten komponent są widoczne, przełącznik motywu zmieni się, aby pasował do nowych ustawień użytkownika, tak jakby użytkownik wszedł w interakcję z przełącznikiem motywu w tym samym momencie, w którym zmienił ustawienia systemu.

Aby to zrobić, użyj JavaScriptu i zdarzenia matchMedia, które nasłuchuje zmian w zapytaniu o multimedia:

window
  .matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', ({matches:isDark}) => {
    theme.value = isDark ? 'dark' : 'light'
    setPreference()
  })
Zmiana ustawień systemu MacOS zmienia stan przełącznika motywu.

Podsumowanie

Teraz, gdy już wiesz, jak to zrobić, jak Ty to zrobisz? 🙂

Zróżnicujemy nasze podejścia i poznamy wszystkie sposoby tworzenia stron internetowych. Utwórz wersję demonstracyjną, wyślij mi linki, a ja dodam je do sekcji z remiksami społeczności.

Remiksy społeczności