Uyarlanabilir ve erişilebilir tema anahtarı bileşeni oluşturmaya dair temel bir genel bakış.
Bu yayında, koyu ve açık tema anahtarı bileşeni oluşturma konusundaki 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 dikkate alınması gereken birkaç web mühendisliği hususu 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
ile ilgili değişikliklerin duyurulmasını 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 eklemesi, ekran okuyuculara kullanıcıya neyin değiştiğini aria-live="assertive"
yerine kibarca söylemelerini işaret eder. Bu düğme, aria-label
değerinin ne olduğuna bağlı olarak "açık" veya "koyu" olarak duyurulur.
Ö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 eklendi. Böylece ekran okuyucular, sunumu belirtmek için işaretlendiğinden öğ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?
SVG'nizi, sonucun çok büyük olmadığından veya sayfa düzeni sorunlarına neden olmadığından emin olmak için 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ının kullanabileceği 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ırma ve CSS dönüştürme işlemleri kullanılır.
Stiller
.theme-toggle
stiller
<button>
öğesi, simge şekilleri ve stillerinin kapsayıcısıdır. Bu üst bağlam, SVG'ye aktarılacak uyarlanabilir renkleri ve boyutları barındırır.
İlk olarak düğmeyi daire şeklinde yapın ve varsayılan düğme stillerini kaldırın:
.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 uzak olmasına dikkat edin:
.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ışını belirtmek için fareyle üzerine gelme medya sorgusu kullanarak düğmenin birçok dokunma boyutu kuralına uyması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 içerirken içindeki SVG, görsel ve animasyonlu yönleri içerir. Bu aşamada simge güzelleştirilebilir ve canlandırılabilir.
Açık tema
Ölçeklendirme ve döndürme animasyonlarının SVG şekillerinin ortasından gerçekleşmesi için transform-origin: center center
ayarlarını 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 duruma duyarlı olmalıdır ancak bu aşamada geçişler olmamalıdır. 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 Özel Medya, geçiş ve animasyonları kullanıcının işletim sistemi hareket tercihlerine göre ayarlamanızı kolaylaştırmak için medya sorgusu değişkenleri için taslak CSS spesifikasyonunun söz dizimini kullanmanıza olanak tanır:
@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, akıcı geçişlerle bu efekti elde ederek aya kıyasla daha eğlenceli olur. 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 karanlık konumları zaten ayarlanmıştır. Kullanıcının hareket tercihlerine saygı duyarken bu öğeyi canlandırmak 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ını ölçülü bir şekilde kullanarak HTML özelliğini ana CSS sayfayı boyamadan önce ayarlayabilir, böylece parlama veya renklerin gösterilmesini ö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, JavaScript belleğinde ve belgede temanın değiştirilmesi gerekir. Mevcut tema değerinin incelenmesi ve yeni durumu hakkında karar verilmesi gerekir. Yeni durumu ayarladıktan sonra kaydedin ve belgeyi 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