Podstawowe informacje o tym, jak utworzyć adaptacyjny i dostępny komponent przełącznika motywu.
W tym poście chcę podzielić się przemyśleniami na temat sposobu tworzenia komponentu przełączania między ciemnym a jasnym motywem. Wypróbuj wersję demonstracyjną
Jeśli wolisz film, obejrzyj tę wersję posta w YouTube:
Przegląd
Witryna może udostępniać ustawienia do kontrolowania schematu kolorów zamiast polegać wyłącznie na preferencjach systemu. Oznacza to, że użytkownicy mogą przeglądać strony w trybie innym niż ustawienia systemowe. Na przykład system użytkownika jest w trybie jasnym, ale użytkownik woli, aby witryna wyświetlała się w trybie ciemnym.
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 poznać preferencje użytkownika, aby zapobiec miganiu kolorów strony, a element sterujący musi najpierw zsynchronizować się z systemem, a potem zezwolić na wyjątki przechowywane po stronie klienta.
Znacznik
W przypadku przełącznika należy użyć elementu <button>, ponieważ wtedy możesz korzystać ze zdarzeń interakcji i funkcji udostępnianych przez przeglądarkę, takich jak zdarzenia kliknięć i możliwość ustawienia fokusu.
Przycisk
Przycisk musi mieć klasę do użycia w CSS i identyfikator do użycia w JavaScript.
Dodatkowo, ponieważ zawartość przycisku to ikona, a nie tekst, dodaj atrybut title, aby podać informacje o jego przeznaczeniu. Na koniec dodaj element
[aria-label]
do przechowywania stanu przycisku ikony, aby czytniki ekranu mogły udostępniać stan motywu osobom z wadami wzroku.
<button
class="theme-toggle"
id="theme-toggle"
title="Toggles light & dark"
aria-label="auto"
>
…
</button>
aria-label i aria-live grzecznie
Aby poinformować czytniki ekranu, że zmiany w aria-label powinny być ogłaszane, dodaj do przycisku element aria-live="polite".
<button
class="theme-toggle"
id="theme-toggle"
title="Toggles light & dark"
aria-label="auto"
aria-live="polite"
>
…
</button>
Ten dodatek do kodu informuje czytniki ekranu, aby w uprzejmy sposób, a nie aria-live="assertive", przekazywały użytkownikowi informacje o zmianach. W przypadku tego przycisku będzie odczytywana informacja „jasny” lub „ciemny” w zależności od tego, jaki jest aria-label.
Ikona skalowalnej grafiki wektorowej (SVG)
SVG umożliwia tworzenie wysokiej jakości, skalowalnych kształtów przy użyciu minimalnej ilości kodu. Interakcja z przyciskiem może wywoływać nowe stany wizualne wektorów, dzięki czemu format SVG świetnie nadaje się do tworzenia ikon.
Ten kod SVG należy umieścić w 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 elementów dekoracyjnych, takich jak ikona w przycisku. Oprócz wymaganego atrybutu viewBox w elemencie dodaj wysokość i szerokość z podobnych powodów, dla których obrazy powinny mieć rozmiary wbudowane.
Słońce
![]()
Grafika słońca składa się z okręgu i linii, które w SVG są wygodnie dostępne jako kształty. Element <circle> jest wyśrodkowany przez ustawienie właściwości cx i cy na 12, czyli połowę rozmiaru widocznego obszaru (24), a następnie nadanie mu promienia (r) o wartości 6, co ustawia 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 w następnym kroku, i ma kolor wypełnienia pasujący do koloru tekstu na stronie z wartością currentColor.
Promienie słońca
![]()
Następnie dodawane są linie promieni słonecznych tuż pod okręgiem, wewnątrz elementu <g>
group.
<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 równej currentColor ustawiana jest wartość stroke każdego wiersza. Linie i okręgi tworzą ładne słońce z promieniami.
Księżyc
Aby stworzyć iluzję płynnego przejścia między światłem (słońcem) a ciemnością (księżycem), księżyc jest powiększoną wersją ikony słońca z użyciem 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>
Maski w SVG są bardzo przydatne, ponieważ pozwalają na usuwanie lub uwzględnianie
części innej grafiki za pomocą kolorów białego i czarnego. Ikona słońca zostanie zasłonięta przez kształt księżyca<circle>
z maską SVG, wystarczy przesunąć okrągły kształt do obszaru maski i z niego.
Co się stanie, jeśli CSS się nie wczyta?
Warto przetestować plik SVG tak, jakby CSS się nie wczytał, aby upewnić się, że wynik nie jest zbyt duży ani nie powoduje problemów z układem. Atrybuty wysokości i szerokości wbudowane w SVG oraz użycie currentColor zapewniają minimalne reguły stylu, których przeglądarka może użyć, jeśli CSS się nie wczyta. Dzięki temu można stosować skuteczne style defensywne w przypadku zakłóceń sieciowych.
Układ
Komponent przełącznika motywu ma niewielką powierzchnię, więc do układu nie potrzebujesz siatki ani flexboxa. Zamiast tego używane są pozycjonowanie SVG i przekształcenia CSS.
Style
.theme-toggle stylów
Element <button> jest kontenerem kształtów i stylów ikon. Ten kontekst nadrzędny będzie zawierać adaptacyjne kolory i rozmiary, które zostaną przekazane do SVG.
Pierwszym zadaniem jest przekształcenie przycisku w okrąg 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 system iOS stosuje do przycisków. Na koniec dodaj trochę miejsca wokół obrysu stanu aktywnego:
.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 w przycisku też wymaga stylów. Plik SVG powinien pasować do rozmiaru przycisku, a końce linii powinny być zaokrąglone, aby uzyskać efekt wizualnej miękkości:
.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;
}
}
Dopasowywanie rozmiaru za pomocą zapytania o media hover
Rozmiar ikony przycisku jest nieco mały i wynosi 2rem, co jest w porządku w przypadku użytkowników myszy, ale może być problematyczne w przypadku wskaźnika o mniejszej precyzji, takiego jak palec. Spraw, aby przycisk spełniał wiele wytycznych dotyczących rozmiaru dotykowego, używając zapytania o media 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 element SVG w jego wnętrzu – aspekty wizualne i animowane. W tym miejscu ikona może zyskać atrakcyjny wygląd i nabrać życia.
Jasny motyw
Aby animacje skalowania i obracania odbywały się od środka kształtów SVG, ustaw dla nich wartość transform-origin: center center. Kształty używają tutaj kolorów adaptacyjnych udostępnianych przez przycisk. Księżyc i słońce używają przycisków var(--icon-fill) i var(--icon-fill-hover) do wypełnienia, a promienie słoneczne używają zmiennych do kreski.
.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
Style księżyca muszą usuwać promienie słoneczne, powiększać okrąg słońca i przesuwać maskę okręgu.
.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 w ciemnym motywie nie ma zmian ani przejść kolorów. Komponent przycisku nadrzędnego ma kolory, które są już adaptacyjne w kontekście ciemnym i jasnym. Informacje o przejściu powinny znajdować się za zapytaniem o preferencje użytkownika dotyczące ruchu.
Animacja
Przycisk powinien być funkcjonalny i stanowy, ale bez przejść w tym momencie. W sekcjach poniżej znajdziesz informacje o tym, jak i co się zmienia.
Udostępnianie zapytań o multimedia i importowanie funkcji przejść
Aby ułatwić umieszczanie przejść i animacji za preferencjami ruchu systemu operacyjnego użytkownika, wtyczka PostCSS Custom Media umożliwia używanie składni projektowanej specyfikacji CSS dla zmiennych zapytań o 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 funkcje przejścia CSS, zaimportuj część easings z Open 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ż przejścia księżyca, co osiągniemy dzięki elastycznym krzywym przyspieszenia. Promienie słoneczne powinny lekko podskakiwać podczas obracania się, a środek słońca powinien lekko podskakiwać podczas skalowania.
Domyślne style (jasny motyw) określają przejścia, a style ciemnego motywu – dostosowania 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;
}
}
}
}
Na panelu Animacja w Narzędziach deweloperskich w Chrome znajdziesz oś czasu przejść animacji. Możesz sprawdzić czas trwania całej animacji, elementów i przejść.
Księżyc
Pozycje księżyca w jasnej i ciemnej wersji są już ustawione. Dodaj style przejścia w zapytaniu o media --motionOK, aby ożywić animację, zachowując przy tym preferencje użytkownika dotyczące ruchu.
Aby przejście było płynne, kluczowe znaczenie ma odpowiednie opóźnienie i czas trwania. Jeśli na przykład słońce zostanie zasłonięte zbyt wcześnie, przejście nie będzie wyglądać na zorganizowane ani zabawne, tylko chaotyczne.
.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;
}
}
}
}
Preferuje mniej animacji
W większości wyzwań związanych z GUI staram się zachować animacje, takie jak płynne przejścia, dla użytkowników, którzy wolą ograniczone ruchy. Ten komponent lepiej działał jednak w przypadku natychmiastowych zmian stanu.
JavaScript
Ten komponent wykonuje wiele zadań w JavaScript, od zarządzania informacjami ARIA dla czytników ekranu po pobieranie i ustawianie wartości z pamięci lokalnej.
Jakość wczytywania strony
Ważne było, aby przy wczytywaniu strony nie występowało miganie kolorów. Jeśli użytkownik z ciemnym schematem kolorów wskaże, że woli jasny schemat, a następnie ponownie załaduje stronę, początkowo będzie ona ciemna, a potem na chwilę stanie się jasna.
Aby temu zapobiec, musieliśmy uruchomić niewielką ilość blokującego kodu JavaScript, którego celem było jak najszybsze ustawienie atrybutu HTML data-theme.
<script src="./theme-toggle.js"></script>
W tym celu najpierw wczytywany jest zwykły tag <script> w dokumencie <head>, a dopiero potem CSS i znaczniki <body>. Gdy przeglądarka napotka nieoznaczony skrypt, taki jak ten, uruchamia kod i wykonuje go przed resztą kodu HTML. Używając tego momentu blokowania oszczędnie, można ustawić atrybut HTML przed renderowaniem strony przez główny arkusz CSS, co zapobiega migotaniu lub wyświetlaniu kolorów.
Najpierw JavaScript 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 analizowana jest funkcja ustawiająca preferencje użytkownika w pamięci lokalnej:
const setPreference = () => {
localStorage.setItem(storageKey, theme.value)
reflectPreference()
}
Następnie 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żną rzeczą, na którą należy zwrócić uwagę w tym momencie, jest stan analizowania dokumentu HTML. Przeglądarka nie zna jeszcze przycisku „#theme-toggle”, ponieważ tag <head> nie został w pełni przeanalizowany. Przeglądarka ma jednak element document.firstElementChild, czyli tag <html>. Funkcja próbuje ustawić oba tagi, aby zachować ich synchronizację, ale przy pierwszym uruchomieniu będzie w stanie ustawić tylko tag HTML. Na początku
querySelector
nie znajdzie niczego, a operator łańcucha opcjonalnego
zapewni, że w przypadku braku elementu nie wystąpią błędy składni, a funkcja setAttribute nie zostanie wywołana.
Następnie funkcja reflectPreference() jest natychmiast wywoływana, aby w dokumencie HTML ustawić atrybut data-theme:
reflectPreference()
Przycisk nadal potrzebuje atrybutu, więc poczekaj 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
Po kliknięciu przycisku motyw musi zostać zmieniony w pamięci JavaScript i w dokumencie. Obecną wartość motywu należy sprawdzić i podjąć decyzję o jej nowym stanie. Po ustawieniu nowego stanu zapisz go i zaktualizuj dokument:
const onClick = () => {
theme.value = theme.value === 'light'
? 'dark'
: 'light'
setPreference()
}
Synchronizacja z systemem
Ten przełącznik motywu jest wyjątkowy, ponieważ synchronizuje się z ustawieniami systemu, gdy te się zmieniają. Jeśli użytkownik zmieni ustawienia systemowe, gdy strona i ten komponent są widoczne, przełącznik motywu zmieni się, aby dopasować się do nowych ustawień użytkownika, tak jakby użytkownik wchodził w interakcję z przełącznikiem motywu w tym samym czasie, w którym nastąpiła zmiana ustawień systemowych.
Możesz to zrobić za pomocą JavaScriptu i matchMedia nasłuchiwania zdarzeń związanych ze zmianami w zapytaniu o media:
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', ({matches:isDark}) => {
theme.value = isDark ? 'dark' : 'light'
setPreference()
})
Podsumowanie
Teraz, gdy wiesz, jak to zrobiłem, jak Ty byś to zrobił? 🙂
Urozmaićmy nasze podejście i poznajmy wszystkie sposoby tworzenia treści w internecie. Utwórz demo, wyślij mi na Twitterze linki, a ja dodam je do sekcji remiksów społeczności poniżej.