Uyarlanabilir ve erişilebilir tema anahtarı bileşeni oluşturmaya dair temel bir genel bakış.
Bu gönderide, koyu ve açık tema geçiş bileşeni oluşturma konusunda düşüncelerimi paylaşmak istiyorum. Demoyu deneyin.
Videoyu tercih ediyorsanız bu yayının YouTube sürümünü burada bulabilirsiniz:
Genel Bakış
Bir web sitesi, tamamen sistem tercihine bağlı kalmak yerine renk şemasını kontrol etmek için ayarlar sağlayabilir. Bu, kullanıcıların sistem tercihlerinden farklı bir modda gezinebileceği anlamına gelir. Örneğin, kullanıcının sistemi açık temadadır ancak kullanıcı web sitesinin koyu temada gösterilmesini tercih eder.
Bu özelliği oluştururken web mühendisliğinde dikkat edilmesi gereken birkaç nokta vardır. Örneğin, sayfa renginin yanıp sönmesini önlemek için tarayıcıya tercihin en kısa sürede bildirilmesi gerekir. Ayrıca, kontrolün önce sistemle senkronize edilmesi, ardından istemci tarafında depolanan istisnalara izin verilmesi gerekir.
Brüt kar
Tıklama etkinlikleri ve odaklanılabilirlik gibi tarayıcı tarafından sağlanan etkileşim etkinliklerinden ve özelliklerinden yararlanmak için açma/kapatma düğmesi için bir <button>
kullanılmalıdır.
Düğme
Düğmenin CSS'den kullanılması için bir sınıfa, JavaScript'den kullanılması için de bir kimliğe ihtiyacı vardır.
Ayrıca, düğme içeriği metin yerine simge olduğundan düğmenin amacı hakkında bilgi sağlamak için bir başlık özelliği ekleyin. Son olarak, simge düğmesinin durumunu tutmak için bir [aria-label]
ekleyin. Böylece ekran okuyucular, temanın durumunu görme engelli kullanıcılarla paylaşabilir.
<button
class="theme-toggle"
id="theme-toggle"
title="Toggles light & dark"
aria-label="auto"
>
…
</button>
aria-label
ve aria-live
kibar
Ekran okuyuculara aria-label
değişikliğinin duyurulması gerektiğini belirtmek için düğmeye aria-live="polite"
ekleyin.
<button
class="theme-toggle"
id="theme-toggle"
title="Toggles light & dark"
aria-label="auto"
aria-live="polite"
>
…
</button>
Bu işaretleme ekleme işlemi, ekran okuyuculara aria-live="assertive"
yerine kibarca nelerin değiştiğini bildirme sinyalini verir. Bu düğme kullanıldığında, aria-label
öğesinin ne olduğuna bağlı olarak "açık" veya "koyu" değeri belirtilir.
Ölçeklenebilir vektör grafiği (SVG) simgesi
SVG, minimum işaretlemeyle yüksek kaliteli ve ölçeklenebilir şekiller oluşturmanın bir yolunu sunar. Düğmeyle etkileşim kurmak, vektörler için yeni görsel durumlar tetikleyebilir. Bu da SVG'yi simgeler için ideal hale getirir.
Aşağıdaki SVG işaretlemesi <button>
içine yerleştirilir:
<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
…
</svg>
aria-hidden
, SVG öğesine eklenmiştir. Böylece ekran okuyucular, sunumu belirtmek için işaretlendiğinden bu öğeyi yoksayabilir. Bu, bir düğmenin içindeki simge gibi görsel süslemeler için idealdir. Öğedeki gerekli viewBox
özelliğine ek olarak, resimlerin satır içi boyutlara sahip olmasıyla ilgili benzer nedenlerden dolayı yükseklik ve genişlik ekleyin.
Güneş
Güneş grafiği, SVG'de uygun şekilleri bulunan bir daire ve çizgilerden oluşur. cx
ve cy
özellikleri, görüntü alanı boyutunun (24) yarısı olan 12 değerine ayarlanarak <circle>
ortalandıktan sonra, boyutu belirleyen 6
yarıçapı (r
) verilir.
<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>
Ayrıca maske mülkü, daha sonra oluşturacağınız bir SVG öğesinin kimliğini işaret eder ve son olarak currentColor
ile sayfanın metin rengiyle eşleşen bir dolgu rengi verir.
Güneş ışınları
Ardından, güneş ışığı çizgileri dairenin hemen altına, bir grup öğesi <g>
grubuna eklenir.
<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>
Bu sefer fill değerinin currentColor
olması yerine her satırın stroke değeri ayarlanır. Çizgiler ve daire şekilleri, ışınları olan güzel bir güneş oluşturur.
Ay
Işık (güneş) ile karanlık (ay) arasında sorunsuz bir geçiş yanılsaması oluşturmak için ay, SVG maskesi kullanılarak güneş simgesinin bir uzantısı olarak tasarlanmıştır.
<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>
SVG'deki maskeler güçlüdür ve beyaz ile siyah renklerin başka bir grafiğin bölümlerini kaldırmasına veya dahil etmesine olanak tanır. Güneş simgesi, SVG maskesi içeren bir ay <circle>
şekliyle gölgelenir. Bunun için bir daire şeklini maske alanına girip çıkarmanız yeterlidir.
CSS yüklenmezse ne olur?
Sonucun çok büyük olmadığından veya düzen sorunlarına neden olmadığından emin olmak için SVG'nizi CSS yüklenmemiş gibi test edebilirsiniz. SVG'deki satır içi yükseklik ve genişlik özellikleri ve currentColor
kullanımı, CSS yüklenmezse tarayıcı için minimum stil kuralları sağlar. Bu, ağdaki dalgalanmalara karşı iyi savunma stilleri oluşturur.
Düzen
Tema anahtarı bileşeninin yüzey alanı küçük olduğundan düzen için ızgara veya esnek kutuya ihtiyacınız yoktur. Bunun yerine, SVG konumlandırması ve CSS dönüşümleri kullanılır.
Stiller
.theme-toggle
stil
<button>
öğesi, simge şekilleri ve stillerinin kapsayıcısıdır. Bu ana bağlam, uyarlanabilir renkleri ve boyutları SVG'ye aktaracak şekilde korur.
İlk görev, düğmeyi daire haline getirmek ve varsayılan düğme stillerini kaldırmaktır:
.theme-toggle {
--size: 2rem;
background: none;
border: none;
padding: 0;
inline-size: var(--size);
block-size: var(--size);
aspect-ratio: 1;
border-radius: 50%;
}
Ardından, bazı etkileşim stilleri ekleyin. Fare kullanıcıları için bir imlec stili ekleyin. Hızlı tepki veren bir dokunma deneyimi için touch-action: manipulation
ekleyin.
iOS'in düğmelere uyguladığı yarı şeffaf vurguyu kaldırın. Son olarak, odak durumu dış çizgisinin öğenin kenarından biraz boşluk bırakmasına izin verin:
.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;
}
Düğmenin içindeki SVG'nin de bazı stillere ihtiyacı var. SVG, düğmenin boyutuna sığmalı ve görsel yumuşaklık için çizgi uçları yuvarlatılmalıdır:
.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;
}
}
hover
medya sorgusuyla uyarlanabilir boyutlandırma
Simge düğmesi boyutu 2rem
olarak biraz küçüktür. Bu, fare kullanıcıları için sorun oluşturmaz ancak parmak gibi kaba bir işaretçi için zor olabilir. Boyut artışı belirtmek için fareyle üzerine gelinen medya sorgusu kullanarak düğmenin birçok dokunma boyutu kuralını karşılamasını sağlayın.
.theme-toggle {
--size: 2rem;
…
@media (hover: none) {
--size: 48px;
}
}
Güneş ve ay SVG stilleri
Düğme, tema anahtarı bileşeninin etkileşimli yönlerini korur. İçindeki SVG ise görsel ve animasyonlu unsurları korur. Bu aşamada simge güzelleştirilebilir ve canlandırılabilir.
Açık tema
Animasyonların SVG şekillerinin merkezinden gerçekleşmesi için ölçekleme ve döndürme işlemlerini transform-origin: center center
yapın. Düğme tarafından sağlanan uyarlanabilir renkler burada şekiller tarafından kullanılır. Ay ve güneş için dolgu olarak var(--icon-fill)
ve var(--icon-fill-hover)
düğmeleri, güneş ışınları için ise fırça değişkenleri kullanılır.
.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);
}
}
}
Koyu tema
Ay stillerinde güneş ışınlarının kaldırılması, güneş çemberinin ölçeklendirilmesi ve daire maskesinin taşınması gerekir.
.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;
}
}
}
}
Koyu temada renk değişikliği veya geçiş olmadığını unutmayın. Renkler, üst düğme bileşenine aittir ve koyu ve açık bağlamda zaten uyarlanabilirdir. Geçiş bilgileri, kullanıcının hareket tercihi medya sorgusunun arkasında olmalıdır.
Animasyon
Düğme işlevsel ve durum bilgili olmalı, ancak bu noktada geçiş içermemelidir. Aşağıdaki bölümlerde, nasıl ve nelerin geçiş yapacağı tanımlanmaktadır.
Medya sorgularını paylaşma ve kolaylaştırmaları içe aktarma
PostCSS eklentisi Custom Media, geçişleri ve animasyonları bir kullanıcının işletim sistemi hareket tercihlerinin arkasına yerleştirmeyi kolaylaştırmak amacıyla medya sorgusu değişkenleri için taslak CSS spesifikasyonu söz diziminin kullanılmasını sağlar:
@custom-media --motionOK (prefers-reduced-motion: no-preference);
/* usage example */
@media (--motionOK) {
.sun {
transition: transform .5s var(--ease-elastic-3);
}
}
Benzersiz ve kullanımı kolay CSS geçişleri için Open Props'un easings bölümünü içe aktarın:
@import "https://unpkg.com/open-props/easings.min.css";
/* usage example */
.sun {
transition: transform .5s var(--ease-elastic-3);
}
Güneş
Güneş geçişleri aydan daha eğlenceli olacak ve yumuşak geçişler yaparak bu efekti elde edebileceksiniz. Güneş ışınları dönerken biraz sıçrama yapmalı ve güneşin merkezi ölçeklendirilirken biraz sıçrama yapmalıdır.
Varsayılan (açık tema) stiller geçişleri, koyu tema stilleri ise açık temaya geçişle ilgili özelleştirmeleri tanımlar:
.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;
}
}
}
}
Chrome Geliştirici Araçları'ndaki Animasyon panelinde animasyon geçişlerinin zaman çizelgesini bulabilirsiniz. Toplam animasyonun süresi, öğeler ve yumuşatma zamanlaması incelenebilir.
Ay
Ay ışığı ve koyu konumları zaten ayarlandı. Kullanıcının hareket tercihlerine saygı gösterirken mesajı hayata geçirmek için --motionOK
medya sorgusunun içine geçiş stilleri ekleyin.
Bu geçişin sorunsuz olması için gecikme ve süreyle ilgili zamanlama çok önemlidir. Örneğin, güneş çok erken gölgelenirse geçiş planlı veya eğlenceli değil, kaotik bir hava verir.
.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;
}
}
}
}
İndirgenmiş hareket tercih edilir
Çoğu GUI yarışmasında, hareketi azaltmayı tercih eden kullanıcılar için opaklık geçişleri gibi bazı animasyonlar kullanmaya çalışıyorum. Ancak bu bileşen, anında durum değişiklikleriyle daha iyi hissettiriyordu.
JavaScript
Bu bileşende, ekran okuyucular için ARIA bilgilerini yönetmekten yerel depolama alanından değer alıp ayarlamaya kadar JavaScript için çok fazla iş vardır.
Sayfa yükleme deneyimi
Sayfanın yüklenmesi sırasında renk yanıp sönmemesi önemlidir. Koyu renk şemasına sahip bir kullanıcı bu bileşenle açık rengi tercih ettiğini belirtip sayfayı yeniden yüklerse sayfa ilk başta koyu olur, ardından açık renkte yanıp söner.
Bunu önlemek için, HTML data-theme
özelliğini olabildiğince erken ayarlamak amacıyla az miktarda engelleme JavaScript'i çalıştırmamız gerekiyordu.
<script src="./theme-toggle.js"></script>
Bunu başarmak için, <head>
dokümanında herhangi bir CSS veya <body>
işaretlemeden önce düz bir <script>
etiketi yüklenir. Tarayıcı, bu gibi işaretlenmemiş bir komut dosyasıyla karşılaştığında kodu çalıştırır ve HTML'nin geri kalanından önce yürütür. Bu engelleme anı ölçülü bir şekilde kullanıldığında, ana CSS sayfayı boyamadan önce HTML özelliğini ayarlayarak flash veya renkleri önleyebilirsiniz.
JavaScript, önce yerel depolama alanında kullanıcının tercihini kontrol eder ve depolama alanında hiçbir şey bulunmazsa sistem tercihini kontrol etmek için yedek yönteme geçer:
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'
}
Ardından, kullanıcının yerel depolama alanındaki tercihini ayarlayan bir işlev ayrıştırılır:
const setPreference = () => {
localStorage.setItem(storageKey, theme.value)
reflectPreference()
}
Ardından, dokümanı tercihlerle değiştirme işlevi gelir.
const reflectPreference = () => {
document.firstElementChild
.setAttribute('data-theme', theme.value)
document
.querySelector('#theme-toggle')
?.setAttribute('aria-label', theme.value)
}
Bu noktada dikkat edilmesi gereken önemli bir nokta, HTML belgesinin ayrıştırma durumudur. <head>
etiketi tamamen ayrıştırılmadığı için tarayıcı henüz "#theme-toggle" düğmesini bilmiyor. Ancak tarayıcıda document.firstElementChild
(<html>
etiketi) vardır. İşlev, senkronize kalması için her ikisini de ayarlamaya çalışır ancak ilk çalıştırmada yalnızca HTML etiketini ayarlayabilir. querySelector
ilk başta hiçbir şey bulamaz ve isteğe bağlı zincirleme operatörü, bulunamadığında ve setAttribute işlevinin çağrılmaya çalışıldığında söz dizimi hatalarının oluşmamasını sağlar.
Ardından, HTML belgesinin data-theme
özelliğinin ayarlanması için reflectPreference()
işlevi hemen çağrılır:
reflectPreference()
Düğmenin özelliğine hâlâ ihtiyacı vardır. Bu nedenle, sayfa yükleme etkinliğini bekleyin. Ardından, aşağıdakileri sorgulamak, dinleyici eklemek ve özellikleri ayarlamak güvenli olacaktır:
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)
}
Geçiş deneyimi
Düğme tıklandığında temanın JavaScript belleğinde ve dokümanda değiştirilmesi gerekir. Mevcut tema değerinin incelenmesi ve yeni durumu hakkında karar verilmesi gerekir. Yeni durum ayarlandıktan sonra yeni durumu kaydedin ve dokümanı güncelleyin:
const onClick = () => {
theme.value = theme.value === 'light'
? 'dark'
: 'light'
setPreference()
}
Sistemle senkronizasyon
Bu tema geçişinin benzersiz özelliği, sistem tercihi değiştikçe senkronizasyon yapmasıdır. Bir kullanıcı, bir sayfa ve bu bileşen görünür durumdayken sistem tercihini değiştirirse tema anahtarı, kullanıcı sistem anahtarını değiştirdiği anda tema anahtarıyla etkileşime girmiş gibi yeni kullanıcı tercihine uyacak şekilde değişir.
Bunu JavaScript ve bir medya sorgusundaki değişiklikleri dinleyen matchMedia
etkinliğiyle yapabilirsiniz:
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', ({matches:isDark}) => {
theme.value = isDark ? 'dark' : 'light'
setPreference()
})
Sonuç
Bunu nasıl yaptığımı öğrendiğinize göre, siz ne yapardınız? 🙂
Yaklaşımlarımızı çeşitlendirelim ve web'de uygulama geliştirmenin tüm yollarını öğrenelim. Bir demo oluşturun, bağlantılarını bana tweetleyin. Ardından, aşağıdaki topluluk remiksleri bölümüne ekleyeceğim.
Topluluk remiksleri
- Vue ile Codepen'de @NathanG
- Codepen'de @ShadowShahriar
- Özel öğe olarak @tomayac
- Vanilya JavaScript ile @bramus
- @JoshWComeau ile react