Uyarlanabilir ve erişilebilir bir tema geçiş bileşeni oluşturmaya yönelik temel bir genel bakış.
Bu yayında, koyu ve açık tema geçiş bileşeni oluşturmanın bir yolu üzerine düşünmek 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 etmeye ilişkin ayarlar sağlayabilir. Bu, kullanıcıların sistem tercihleri dışında bir modda gezinebileceği anlamına gelir. Örneğin, kullanıcının sisteminde açık tema kullanılıyor ancak kullanıcı web sitesinin koyu temada görüntülenmesini tercih ediyor.
Bu özellik oluşturulurken web mühendisliği ile ilgili dikkat edilmesi gereken birkaç nokta vardır. Örneğin, tarayıcı, sayfa rengi yanıp sönmelerini önlemek için mümkün olan en kısa sürede bu tercihe dikkat edilmelidir. Ayrıca kontrolün öncelikle sistemle senkronize edilmesi ve ardından istemci tarafında depolanan istisnalara izin vermesi gerekir.
Markup
Ardından, tarayıcı tarafından sağlanan etkileşim etkinlikleri ve özelliklerinden (ör. tıklama etkinlikleri ve odaklanılabilirlik) yararlanabileceğiniz için açma/kapatma düğmesi için <button>
kullanılmalıdır.
Düğme
Düğmenin, CSS'den kullanılacak bir sınıfa ve JavaScript'ten kullanılacak bir kimliğe ihtiyacı vardır.
Ayrıca, düğme içeriği metin yerine bir simge olduğundan düğmenin amacı hakkında bilgi sağlamak için bir title özelliği ekleyin. Son olarak, simge düğmesinin durumunu korumak için bir [aria-label]
ekleyin. Böylece ekran okuyucular temanın durumunu görme engelli kişilerle 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ı 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 eklemesi, ekran okuyuculara aria-live="assertive"
yerine kibar bir şekilde kullanıcıya neyin değiştiğini söylemesi gerektiğini bildirir. Bu düğmede ise aria-label
öğesinin ne olduğuna bağlı olarak "açık" veya "koyu" değerini duyurur.
Ö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şimde bulunmak, vektörler için yeni görsel durumları tetikleyebilir. Bu da SVG'yi simgeler için mükemmel kılar.
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>
SVG öğesine aria-hidden
eklendi. Böylece ekran okuyucular, bu öğe sunum amaçlı olarak işaretlendiği için bu öğeyi görmezden gelebilir. Bunu, düğmenin içindeki simge gibi görsel süslemeler
için mükemmel bir şekilde yapabilirsiniz. Öğede gerekli viewBox
özelliğine ek olarak, resimlerin satır içi boyutları alması gereken benzer nedenlerle yükseklik ve genişlik ekleyin.
Güneş
Güneş grafiği, SVG'nin kolayca kullanabileceği şekiller içeren bir daire ve çizgilerden oluşur. <circle>
, cx
ve cy
özellikleri 12 olarak ayarlanarak ortalanır. Bu değer, görüntü alanı boyutunun (24) yarısıdır ve ardından 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>
Buna ek olarak, maske özelliği, daha sonra oluşturacağınız bir SVG öğesinin kimliğine işaret eder ve son olarak, currentColor
ile sayfanın metin rengiyle eşleşen bir dolgu rengi verilir.
Güneş ışınlanıyor
Daha sonra, güneş ışını çizgileri bir grup öğesi <g>
grubunun içine dairenin hemen altına 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 kez, fill değeri currentColor
yerine her satırın fırçası ayarlanır. Çizgiler ve daire şekilleri, kirişli güzel bir güneş oluşturuyor.
Ay
Işık (güneş) ve karanlık (ay) arasında kesintisiz bir geçiş illüzyonu yaratmak amacıyla ay, bir SVG maskesi kullanılarak güneş simgesinin genişletilmesidir.
<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 içeren maskeler güçlüdür, beyaz ve siyah renklerin başka bir grafiğin parçalarını kaldırmasına veya içermesine olanak tanır. SVG maskeli bir ay <circle>
şekline yansıtılır. Bunun için, maske alanının içine ve dışına bir daire şekli taşıması 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 etmek yararlı olabilir. SVG'deki satır içi yükseklik ve genişlik özellikleri ile currentColor
kullanımı, CSS yüklenmezse tarayıcının kullanacağı minimum stil kurallarını sağlar. Bu da ağ düzensizliğine karşı iyi savunma stilleri sağlar.
Düzen
Tema değiştirme bileşeninin yüzey alanı çok az olduğundan düzen için kılavuza veya flexbox'a ihtiyacınız olmaz. Bunun yerine, SVG konumlandırması ve CSS dönüşümleri kullanılır.
Stiller
.theme-toggle
stil
<button>
öğesi, simge şekilleri ve stilleri için kapsayıcıdır. Bu üst bağlam, SVG'ye aktarılacak uyarlanabilir renkleri ve boyutları saklayacaktır.
İlk görev, düğmeyi bir daire yapmak 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, birkaç etkileşim stili ekleyin. Fare kullanıcıları için imleç stili ekleyin. Hızlı tepki veren dokunma deneyimi için touch-action: manipulation
ekleyin.
iOS'in düğmelere uyguladığı yarı şeffaf vurgulamayı kaldırın. Son olarak, odak durumunun ana hatlarına nesnenin kenarından biraz boşluk bırakı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%;
cursor: pointer;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
outline-offset: 5px;
}
Düğmenin içindeki SVG'nin de bazı stilleri olmalıdır. SVG, düğmenin boyutuna sığmalı ve görsel yumuşaklık için çizgi uçlarını yuvarlamalı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üğmesinin boyutu 2rem
için biraz küçüktür. Bu, fare kullanıcıları için uygundur ancak parmak gibi genel işaretçiler konusunda zorlanabilir. Boyut artışını belirtmek için fareyle üzerine gelme medya sorgusu kullanarak düğmenin birçok dokunma boyutu yönergesini karşılamasını sağlayın.
.theme-toggle {
--size: 2rem;
…
@media (hover: none) {
--size: 48px;
}
}
Güneş ve ay SVG stilleri
Düğme, tema değiştirme bileşeninin etkileşimli yönlerini tutarken SVG içerideki görsel ve animasyonlu öğeleri barındırır. Simge burada güzelleştirilebilir ve hayata geçirilebilir.
Açık tema
Animasyonların SVG şekillerinin merkezinden gerçekleşmesi için ölçeklendirme ve döndürme işlemlerini yapmak üzere bunların transform-origin: center center
değerini ayarlayın. Düğmenin sağladığı uyarlanabilir renkler burada
şekiller tarafından kullanılır. Ay ve güneş, dolguları için var(--icon-fill)
ve var(--icon-fill-hover)
düğmelerini kullanırken güneş ışınları inme için değişkenleri kullanı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 stillerinin güneş ışınlarını kaldırması, güneş dairesini ölçeklendirmesi ve daire maskesini hareket ettirmesi gerekir.
.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;
}
}
}
}
Koyu temada renk değişikliği veya geçiş yapılmadığına dikkat edin. Üst düğme bileşeni, karanlık ve açık bağlamda zaten uyarlanabilir olan renklerin sahibidir. Geçiş bilgisi, kullanıcının hareket tercihi medya sorgusunun arkasında olmalıdır.
Animasyonlar
Düğme, çalışır durumda ve durum bilgili olmalı, ancak bu noktada geçişler içermemelidir. Aşağıdaki bölümler, nasıl ve ne geçişleri tanımlamakla ilgilidir.
Medya sorgularını paylaşma ve yumuşatmaları içe aktarma
PostCSS eklentisi Custom Media, geçişleri ve animasyonları 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ımını etkinleştirir:
@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 yumuşatmaları için Open Props'un yumuşatmalar 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 zıplamaları yumuşatarak bu etkiyi sağlayacak. Güneş ışınları dönerken biraz zıplamalı ve güneşin merkezi ölçeklenirken az miktarda zıplamalıdır.
Varsayılan (açık tema) stiller geçişleri, koyu tema stilleri ise ışığa geçiş için ö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şleri için bir zaman çizelgesi bulabilirsiniz. Toplam animasyonun süresi, öğeler ve yumuşak geçiş zamanlaması incelenebilir.
Ay
Ay ışığı ve koyu konumları zaten ayarlandı. Kullanıcının hareket tercihlerine uygun şekilde hareket etmesi için --motionOK
medya sorgusunun içine geçiş stilleri ekleyin.
Gecikmeli zamanlama ve süre, bu geçişi netleştirmede kritik öneme sahiptir. Örneğin, güneş çok erken alınırsa bu geçiş düzenli veya eğlenceli hissettirmez, kaotik bir his olur.
.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;
}
}
}
}
Azaltılmış hareketi tercih eder
Çoğu GUI Meydan Okuması'nda, hareket düzeyini azaltan kullanıcılar için opaklık çapraz geçişi gibi animasyonlar tutmaya çalışıyorum. Ancak bu bileşen, anlık durum değişiklikleriyle daha iyi hissetti.
JavaScript
Bu bileşende, ekran okuyucular için ARIA bilgilerinin yönetilmesinden yerel depolama alanından değer almaya ve ayarlamaya kadar JavaScript'e yönelik pek çok işlem gerçekleştirilir.
Sayfa yükleme deneyimi
Sayfa yüklenirken renk yanıp sönmemesi önemliydi. Koyu renk şemasına sahip bir kullanıcı bu bileşenle ışığı tercih ettiğini belirtirse ve ardından sayfayı yeniden yüklediğinde önce sayfa koyu renk olur, sonra da yanıp söner.
Bunun önlenmesi, data-theme
HTML özelliğini mümkün olan en kısa sürede ayarlamak amacıyla az miktarda engelleme JavaScript'i çalıştırmak anlamına geliyordu.
<script src="./theme-toggle.js"></script>
Bunun için <head>
dokümanındaki düz <script>
etiketi, herhangi bir CSS veya <body>
işaretlemesinden önce yüklenir. Tarayıcı bunun 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 yanıp sönmeyi veya renkleri engelleyebilirsiniz.
JavaScript, önce yerel depolama alanında kullanıcının tercihini kontrol eder ve depolama alanında hiçbir şey bulunamazsa sistem tercihini kontrol etmek için geri döner:
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ümanda tercihlerle değişiklik yapmak için bir işlev gösterilir.
const reflectPreference = () => {
document.firstElementChild
.setAttribute('data-theme', theme.value)
document
.querySelector('#theme-toggle')
?.setAttribute('aria-label', theme.value)
}
Bu noktada unutulmaması gereken önemli bir nokta da HTML
belgesi ayrıştırma durumudur. <head>
etiketi tam olarak ayrıştırılmadığından tarayıcı henüz "#theme-toggle" düğmesini tanımaz. Ancak tarayıcıda <html>
etiketi olan bir document.firstElementChild
var. İşlev, her ikisini de senkronize halde tutmak için her ikisini de ayarlamayı dener, ancak ilk çalıştırmada yalnızca HTML etiketi ayarlanabilir. querySelector
başta hiçbir şey bulmaz ve isteğe bağlı zincirleme operatörü, bulunamadığında söz dizimi hatasının olmamasını ve setAttribute işlevi çağrılmaya çalışılmasını sağlar.
Daha sonra, bu reflectPreference()
işlevi hemen çağrılır ve HTML belgesinde data-theme
özelliği ayarlanır:
reflectPreference()
Düğmenin hâlâ bu özelliğe ihtiyacı vardır. Bu nedenle sayfa yükleme etkinliğini bekleyin. Sorgulama, işleyici ekleme ve özellikleri ayarlama işlemi 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 durumuyla ilgili bir karar verilmesi gerekir. Yeni durum ayarlandıktan sonra durumu kaydedin ve dokümanı güncelleyin:
const onClick = () => {
theme.value = theme.value === 'light'
? 'dark'
: 'light'
setPreference()
}
Sistemle senkronize ediliyor
Bu tema anahtarında yalnızca, değişiklik değiştikçe sistem tercihiyle senkronizasyon bulunur. Kullanıcı, bir sayfa ve bu bileşen görünür durumdayken sistem tercihini değiştirirse tema anahtarı, kullanıcının sisteme geçiş yaptığı sırada tema anahtarıyla etkileşimde bulunmuş gibi olması gibi yeni kullanıcı tercihine uyacak şekilde değişir.
Bunu JavaScript ve medya sorgusundaki değişiklikleri dinlemede bir matchMedia
etkinliğiyle gerçekleştirin:
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', ({matches:isDark}) => {
theme.value = isDark ? 'dark' : 'light'
setPreference()
})
Sonuç
Nasıl yaptığımı artık bildiğine göre siz de nasıl yapardınız? 🙂
Yaklaşımlarımızı çeşitlendirelim ve web'de geliştirme yapmanın tüm yollarını öğrenelim. Bir demo oluşturun, bana tweet atın bağlantıları, aşağıdaki topluluk remiksleri bölümüne ekleyeceğim.
Topluluk remiksleri
- Codepen with Vue'da @NathanG
- Codepen üzerinde @ShadowShahriar
- Özel öğe olarak @tomayac
- vanilla JavaScript ile @bramus
- @JoshWComeau ile react