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

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-label
i aria-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
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 cx
i cy
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,
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>

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?

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

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)
i 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

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ęść 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 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.


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


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()
})
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.