Ringkasan dasar tentang cara membangun komponen pengalihan tema yang adaptif dan mudah diakses.
Dalam postingan ini, saya ingin berbagi pemikiran tentang cara membangun komponen tombol akses tema gelap dan terang. Coba demonya.
Jika Anda lebih suka video, berikut versi YouTube postingan ini:
Ringkasan
Situs dapat menyediakan setelan untuk mengontrol skema warna, bukan hanya mengandalkan preferensi sistem sepenuhnya. Artinya, pengguna dapat menjelajah dalam mode selain preferensi sistem mereka. Misalnya, sistem pengguna menggunakan tema terang, tetapi pengguna lebih suka situs ditampilkan dalam tema gelap.
Ada beberapa pertimbangan rekayasa web saat membuat fitur ini. Misalnya, browser harus menyadari preferensi sesegera mungkin untuk mencegah warna halaman berkedip, dan kontrol harus disinkronkan terlebih dahulu dengan sistem kemudian mengizinkan pengecualian yang disimpan dari sisi klien.
Markup
<button>
harus digunakan untuk tombol tersebut, karena Anda kemudian akan mendapatkan manfaat dari fitur dan
peristiwa interaksi yang disediakan browser, seperti peristiwa klik dan kemampuan fokus.
Tombol
Tombol memerlukan class untuk digunakan dari CSS dan ID untuk digunakan dari JavaScript.
Selain itu, karena konten tombol adalah ikon, bukan teks, tambahkan
atribut title
untuk memberikan informasi tentang tujuan tombol. Terakhir, tambahkan
[aria-label]
untuk menyimpan status tombol ikon, sehingga pembaca layar dapat membagikan status
tema kepada pengguna yang memiliki gangguan penglihatan.
<button
class="theme-toggle"
id="theme-toggle"
title="Toggles light & dark"
aria-label="auto"
>
…
</button>
aria-label
dan aria-live
sopan
Untuk menunjukkan kepada pembaca layar bahwa perubahan pada aria-label
harus diumumkan,
tambahkan
aria-live="polite"
ke tombol tersebut.
<button
class="theme-toggle"
id="theme-toggle"
title="Toggles light & dark"
aria-label="auto"
aria-live="polite"
>
…
</button>
Markup ini memberi sinyal kepada pembaca layar untuk dengan sopan, bukan aria-live="assertive"
,
memberi tahu pengguna apa yang berubah. Dalam kasus tombol ini, tombol akan mengumumkan "terang"
atau "gelap" bergantung pada apa yang menjadi aria-label
.
Ikon Scalable Vector Graphics (SVG)
SVG menyediakan cara untuk membuat bentuk yang skalabel dan berkualitas tinggi dengan markup minimal. Berinteraksi dengan tombol dapat memicu status visual baru untuk vektor, sehingga SVG sangat bagus untuk ikon.
Markup SVG berikut masuk ke dalam <button>
:
<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
…
</svg>
aria-hidden
telah ditambahkan ke elemen SVG sehingga pembaca layar tahu untuk mengabaikannya karena
ditandai sebagai presentasi. Ini bagus untuk dilakukan untuk dekorasi visual, seperti ikon
di dalam tombol. Selain atribut viewBox
yang diperlukan pada elemen,
tambahkan tinggi dan lebar untuk alasan serupa bahwa gambar harus mendapatkan ukuran
inline.
Matahari
Grafik matahari terdiri dari lingkaran dan garis-garis yang bentuk SVG mudahnya
dalam hal ini. <circle>
dipusatkan dengan menetapkan properti cx
dan cy
ke 12,
yang merupakan setengah dari ukuran area pandang (24), lalu diberi radius (r
) 6
yang menetapkan ukuran.
<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>
Selain itu, properti mask mengarah ke ID elemen
SVG,
yang akan Anda buat selanjutnya, dan terakhir diberi warna isian yang cocok dengan
warna teks halaman dengan
currentColor
.
Cahaya matahari
Selanjutnya, garis sinar matahari ditambahkan tepat di bawah lingkaran, di dalam grup
elemen
grup <g>
.
<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>
Kali ini, alih-alih nilai
fill yang menjadi
currentColor
, stroke setiap
baris akan
ditetapkan. Garis-garis ditambah bentuk lingkaran menciptakan matahari yang indah dengan balok-baloknya.
Bulan
Untuk menciptakan ilusi transisi yang lancar antara terang (matahari) dan gelap (bulan), bulan merupakan augmentasi ikon matahari, menggunakan topeng 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>
Mask dengan SVG
sangat efektif, sehingga warna putih dan hitam dapat menghapus atau menyertakan
bagian dari grafis lain. Ikon matahari akan dihalangi oleh bentuk
<circle>
bulan dengan mask SVG, cukup dengan memindahkan bentuk lingkaran ke dalam dan ke luar
area mask.
Apa yang terjadi jika CSS tidak dapat dimuat?
Sebaiknya uji SVG seolah-olah CSS tidak dimuat untuk memastikan hasilnya
tidak terlalu besar atau menyebabkan masalah tata letak. Atribut tinggi dan lebar inline pada
SVG ditambah penggunaan currentColor
memberikan aturan gaya minimal untuk browser
yang digunakan jika CSS tidak dimuat. Cara ini menghasilkan gaya defensif yang
bagus terhadap turbulensi jaringan.
Tata Letak
Komponen tombol tema memiliki sedikit area permukaan, sehingga Anda tidak memerlukan petak atau flexbox untuk tata letak. Sebagai gantinya, pemosisian SVG dan transformasi CSS digunakan.
Gaya
.theme-toggle
gaya
Elemen <button>
adalah penampung untuk bentuk dan gaya ikon. Konteks
induk ini akan menyimpan warna dan ukuran adaptif untuk diteruskan ke SVG.
Tugas pertama adalah membuat tombol menjadi lingkaran dan menghapus gaya tombol default:
.theme-toggle {
--size: 2rem;
background: none;
border: none;
padding: 0;
inline-size: var(--size);
block-size: var(--size);
aspect-ratio: 1;
border-radius: 50%;
}
Selanjutnya, tambahkan beberapa gaya interaksi. Tambahkan gaya kursor untuk pengguna mouse. Tambahkan
touch-action: manipulation
untuk pengalaman sentuh
reaksi cepat.
Hapus sorotan semi-transparan yang diterapkan iOS ke tombol. Terakhir, berikan
status fokus untuk menguraikan beberapa ruang pernapasan dari tepi elemen:
.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 di dalam tombol juga membutuhkan beberapa gaya. SVG harus pas dengan ukuran tombol dan, untuk kelembutan visual, bulatkan ujung baris:
.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;
}
}
Ukuran adaptif dengan kueri media hover
Ukuran tombol ikon agak kecil di 2rem
, yang bagus bagi pengguna mouse, tetapi
mungkin sulit untuk pointer kasar seperti jari. Buat tombol sesuai dengan banyak
pedoman
ukuran sentuh
dengan menggunakan kueri media
arahkan kursor untuk menentukan
peningkatan ukuran.
.theme-toggle {
--size: 2rem;
…
@media (hover: none) {
--size: 48px;
}
}
Gaya SVG matahari dan bulan
Tombol ini menampung aspek interaktif dari komponen switch tema, sementara SVG di dalamnya akan menyimpan aspek visual dan animasi. Di sinilah ikon dapat dibuat indah dan menjadi hidup.
Tema terang
Agar animasi diskalakan dan diputar dari pusat bentuk SVG, tetapkan
transform-origin: center center
-nya. Warna adaptif yang disediakan oleh
tombol digunakan di sini oleh bentuk. Bulan dan matahari menggunakan tombol yang disediakan
var(--icon-fill)
dan var(--icon-fill-hover)
untuk pengisinya, sedangkan
sinar matahari menggunakan variabel untuk goresan.
.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);
}
}
}
Tema gelap
Gaya bulan perlu menghilangkan sinar matahari, meningkatkan skala lingkaran matahari, dan memindahkan mask lingkaran.
.sun-and-moon {
@nest [data-theme="dark"] & {
& > .sun {
transform: scale(1.75);
}
& > .sun-beams {
opacity: 0;
}
& > .moon > circle {
transform: translateX(-7px);
@supports (cx: 1) {
transform: translateX(0);
cx: 17;
}
}
}
}
Perhatikan bahwa tema gelap tidak memiliki perubahan atau transisi warna. Komponen tombol induk memiliki warna, yang sudah adaptif dalam konteks gelap dan terang. Informasi transisi harus berada di belakang kueri media preferensi gerakan pengguna.
Animasi
Tombol harus berfungsi dan stateful, tetapi tanpa transisi pada tahap ini. Bagian berikut membahas semuanya tentang menentukan transisi bagaimana dan apa.
Berbagi kueri media dan mengimpor easing
Untuk memudahkan penempatan transisi dan animasi di belakang preferensi gerakan sistem operasi pengguna, plugin PostCSS Custom Media memungkinkan penggunaan sintaksis spesifikasi CSS yang disusun untuk variabel kueri media:
@custom-media --motionOK (prefers-reduced-motion: no-preference);
/* usage example */
@media (--motionOK) {
.sun {
transition: transform .5s var(--ease-elastic-3);
}
}
Untuk easing CSS yang unik dan mudah digunakan, impor bagian easing dari Props Terbuka:
@import "https://unpkg.com/open-props/easings.min.css";
/* usage example */
.sun {
transition: transform .5s var(--ease-elastic-3);
}
Matahari
Transisi matahari akan lebih menyenangkan daripada bulan, mencapai efek ini dengan easing memantul. Sinar matahari akan memantul sedikit saat berotasi dan bagian tengah matahari akan memantul kecil seiring perubahan skalanya.
Gaya default (tema terang) menentukan transisi dan gaya tema gelap menentukan penyesuaian untuk transisi ke terang:
.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;
}
}
}
}
Di panel Animasi di Chrome DevTools, Anda dapat menemukan linimasa untuk transisi animasi. Durasi total animasi, elemen, dan waktu easing dapat diperiksa.
Bulan
Posisi terang dan gelap bulan sudah ditetapkan, tambahkan gaya transisi di dalam
kueri media --motionOK
untuk membuatnya lebih hidup sekaligus mengikuti preferensi
gerakan pengguna.
Penentuan waktu dengan penundaan dan durasi sangat penting untuk membuat transisi ini bersih. Jika matahari muncul terlalu awal, misalnya, transisi tidak terasa diatur atau menyenangkan, transisi akan terasa kacau.
.sun-and-moon {
@media (--motionOK) {
& .moon > circle {
transform: translateX(-7px);
transition: transform .25s var(--ease-out-5);
@supports (cx: 1) {
transform: translateX(0);
cx: 17;
transition: cx .25s var(--ease-out-5);
}
}
@nest [data-theme="dark"] & {
& > .moon > circle {
transition-delay: .25s;
transition-duration: .5s;
}
}
}
}
Memilih gerakan yang dikurangi
Di sebagian besar GUI Challenges, saya mencoba mempertahankan beberapa animasi, seperti opasitas silang yang memudar, untuk pengguna yang lebih menyukai gerakan yang dikurangi. Namun, komponen ini terasa lebih baik dengan perubahan status instan.
JavaScript
Ada banyak pekerjaan untuk JavaScript dalam komponen ini, mulai dari mengelola informasi ARIA untuk pembaca layar hingga mendapatkan dan menyetel nilai dari penyimpanan lokal.
Pengalaman pemuatan halaman
Perhatikan bahwa tidak ada flash warna yang terjadi saat pemuatan halaman. Jika pengguna dengan
skema warna gelap menunjukkan bahwa mereka lebih memilih cahaya dengan komponen ini,
maka halaman dimuat ulang, pada awalnya halaman akan gelap, lalu akan berkedip menjadi terang.
Untuk mencegah hal ini, Anda harus menjalankan pemblokiran JavaScript dalam jumlah kecil dengan tujuan menetapkan atribut HTML data-theme
sedini mungkin.
<script src="./theme-toggle.js"></script>
Untuk mencapai hal ini, tag <script>
biasa dalam <head>
dokumen dimuat terlebih dahulu, sebelum markup CSS atau <body>
. Saat menemukan
skrip yang tidak ditandai seperti ini, browser akan menjalankan kode dan mengeksekusinya sebelum
HTML lainnya. Dengan menggunakan momen pemblokiran ini dengan hemat, atribut HTML dapat disetel sebelum CSS utama menggambar halaman, sehingga mencegah flash atau warna.
JavaScript akan memeriksa preferensi pengguna dalam penyimpanan lokal terlebih dahulu untuk memeriksa preferensi sistem jika tidak ada yang ditemukan dalam penyimpanan:
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'
}
Fungsi untuk menetapkan preferensi pengguna di penyimpanan lokal akan diuraikan berikutnya:
const setPreference = () => {
localStorage.setItem(storageKey, theme.value)
reflectPreference()
}
Diikuti oleh fungsi untuk memodifikasi dokumen dengan preferensi.
const reflectPreference = () => {
document.firstElementChild
.setAttribute('data-theme', theme.value)
document
.querySelector('#theme-toggle')
?.setAttribute('aria-label', theme.value)
}
Hal penting yang perlu diperhatikan pada tahap ini adalah
status penguraian dokumen HTML. Browser belum mengetahui tentang tombol "#theme-toggle",
karena tag <head>
belum diurai sepenuhnya. Namun, browser memiliki
document.firstElementChild
, alias tag <html>
. Fungsi ini mencoba menetapkan keduanya agar keduanya tetap sinkron, tetapi saat dijalankan pertama kali hanya dapat menetapkan tag HTML. Pada awalnya, querySelector
tidak akan menemukan apa pun dan operator rantai opsional memastikan tidak ada error sintaksis jika tidak ditemukan dan fungsi setAttribute dicoba untuk dipanggil.
Selanjutnya, fungsi reflectPreference()
tersebut segera dipanggil sehingga dokumen HTML memiliki kumpulan atribut data-theme
:
reflectPreference()
Tombol masih memerlukan atribut, jadi tunggu peristiwa pemuatan halaman, lalu kueri akan aman untuk dibuat, menambahkan pemroses, dan menetapkan atribut di:
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)
}
Pengalaman beralih
Saat tombol diklik, tema perlu ditukar, dalam memori JavaScript dan dalam dokumen. Nilai tema saat ini harus diperiksa dan keputusan yang dibuat tentang status barunya. Setelah status baru ditetapkan, simpan dan perbarui dokumen:
const onClick = () => {
theme.value = theme.value === 'light'
? 'dark'
: 'light'
setPreference()
}
Menyinkronkan dengan sistem
Hal unik untuk tombol tema ini adalah sinkronisasi dengan preferensi sistem saat berubah. Jika pengguna mengubah preferensi sistem saat halaman dan komponen ini terlihat, tombol tema akan berubah agar sesuai dengan preferensi pengguna baru, seolah-olah pengguna telah berinteraksi dengan tombol tema pada saat yang sama saat peralihan sistem terjadi.
Capai ini dengan JavaScript dan peristiwa
matchMedia
yang memproses perubahan pada kueri media:
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', ({matches:isDark}) => {
theme.value = isDark ? 'dark' : 'light'
setPreference()
})
Kesimpulan
Setelah Anda tahu cara saya melakukannya, bagaimana Anda‽ 🙂
Mari lakukan diversifikasi pendekatan dan pelajari semua cara untuk membangun di web. Buat demo, link tweet me, dan saya akan menambahkannya ke bagian remix komunitas di bawah.
Remix komunitas
- @NathanG di Codepen dengan Vue
- @ShadowShahriar di Codepen
- @tomayac sebagai elemen kustom
- @bramus dengan vanilla JavaScript
- @JoshWComeau dengan react