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ą.

prezentacji, aby zwiększyć 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ż ich ustawienia systemowe. Na przykład system użytkownika korzysta z jasnego motywu, ale użytkownik woli wyświetlać witrynę 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ż zawartość 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-label i aria-live – grzecznościowe

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)

Format SVG umożliwia tworzenie wysokiej jakości skalowanych kształtów z minimalną ilością znaczników. Interakcje z przyciskiem mogą aktywować nowe stany wizualne wektorów, dzięki czemu SVG świetnie nadają się do stosowania 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 to element oznaczony jako 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 swoich kształtów. Element <circle> jest wyśrodkowany, jeśli we właściwościach cx i cy ustawisz wartość 12, czyli połowa rozmiaru widocznego obszaru (24), a następnie otrzyma on promień (r) równy 6, który 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 nadasz mu kolor wypełnienia pasujący do koloru tekstu na stronie (currentColor).

promienie słoneczne,

Ikona słońca z wyblakłym środkiem i czerwonoróżową strzałką wskazującą 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żdego wiersza. Linie i koła tworzą słońce z promienie.

Księżyc

Aby stworzyć iluzję płynnego przejścia między światłem (słońce) a ciemnością (księżyc), księżyc jest uzupełnioną ikoną 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 przydatne, 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ę CSS?

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

Warto przetestować plik SVG tak, jakby plik CSS się nie wczytywał, 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. Pozwala to uzyskać dobry styl obrony przed turbulencją sieci.

Układ

Element 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> jest kontenerem dla kształtów i stylów ikon. Ten kontekst nadrzędny będzie zawierać kolory i rozmiary dostosowane do różnych urządzeń, 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 style 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 też wymaga pewnych stylów. Plik SVG powinien pasować do rozmiaru przycisku, a końce wierszy powinny być bardziej gładkie:

.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;
  }
}

Adaptacyjne rozmiary za pomocą zapytania o multimedia 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ńce i księżyc

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 wykorzystują przyciski var(--icon-fill) i var(--icon-fill-hover) do wypełnienia, a promienie słoneczne wykorzystują zmienne określające ruch.

.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 stylach Księżyca należy usunąć promienie słoneczne, zwiększyć skalę okręgu słonecznego 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 stanowy, ale na tym etapie nie może zawierać przejść. W kolejnych sekcjach określamy jak i co przechodzi w ramach przejścia.

Udostępnianie zapytań o multimedia i importowanie wygładzania

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 dynamiczne niż w przypadku 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 ciemnego motywu definiują opcje przejścia do jasnego motywu:

​​.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 oś 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 ożywić animację, zachowując przy tym preferencje użytkownika.

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 z ciemnym schematem kolorów wskazuje, że preferuje jasne światło z tym komponentem, a następnie ponownie załaduje stronę, na początku będzie ona ciemna, a potem zacznie migać i zaświecić. Aby temu zapobiec, trzeba było uruchomić niewielką ilość blokującego kodu JavaScript, aby jak najszybciej ustawić atrybut HTML data-theme.

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

W tym celu najpierw wczytuje się zwykły tag <script> w dokumencie <head>, zanim zostanie załadowany dowolny znacznik CSS lub <body>. Gdy przeglądarka napotka taki niezaznaczony skrypt, uruchomi go przed resztą kodu HTML. Oszczędnie możesz wykorzystać ten moment, by ustawić atrybut HTML przed wyrenderowaniem strony przez główny CSS, co zapobiega pojawieniu się błysków lub 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()
}

Obok niej znajduje się 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 zawiera tag document.firstElementChild, czyli tag <html>. Funkcja próbuje je skonfigurować, by zapewnić ich synchronizację, ale przy pierwszym uruchomieniu może ustawić tylko tag HTML. Funkcja querySelector na początku niczego nie znajdzie, a operator łańcucha opcjonalnego zapobiega błędom składni, gdy nie zostanie znaleziony obiekt, a funkcja setAttribute zostanie wywołana.

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 wczytywania 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)
}

Sposób przełączania

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 systemowymi 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 matchMediazdarzenia nasłuchującego zmiany w zapytaniu o multimedia:

window
  .matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', ({matches:isDark}) => {
    theme.value = isDark ? 'dark' : 'light'
    setPreference()
  })
Zmiana preferencji systemu macOS powoduje zmianę stanu przełącznika motywu

Podsumowanie

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

Stosujmy różne podejścia i poznajmy sposoby budowania obecności w internecie. Utwórz wersję demonstracyjną, wyślij mi linki, a ja dodam je do sekcji z remiksami społeczności.

Remiksy utworzone przez społeczność