Uyarlanabilir ve erişilebilir bir tema değiştirme bileşeni oluşturma hakkında temel bilgiler.
Bu yayında, koyu ve açık tema arasında geçiş yapmaya olanak tanıyan bir bileşen oluşturma yöntemiyle ilgili düşüncelerimi paylaşmak istiyorum. Demoyu deneyin.
Video tercih ediyorsanız bu yayının YouTube versiyonunu aşağıda bulabilirsiniz:
Genel Bakış
Bir web sitesi, tamamen sistem tercihine güvenmek yerine renk şemasını kontrol etmek için ayarlar sağlayabilir. Bu, kullanıcıların sistem tercihlerinden farklı bir modda göz atabileceğ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 çeşitli web mühendisliği hususları vardır. Örneğin, sayfa renginin yanıp sönmesini önlemek için tarayıcı tercihten mümkün olduğunca kısa sürede haberdar edilmelidir. Kontrolün önce sistemle senkronize olması, ardından istemci tarafında depolanan istisnalara izin vermesi gerekir.

Brüt kar
Açma/kapatma düğmesi için <button>
kullanılmalıdır. Böylece, tarayıcı tarafından sağlanan etkileşim etkinliklerinden ve özelliklerden (ör. tıklama etkinlikleri ve odaklanabilirlik) yararlanabilirsiniz.
Düğmesi
Düğmenin CSS'den kullanılmak üzere bir sınıfa ve JavaScript'ten kullanılmak üzere 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 title özelliğini ekleyin. Son olarak, ekran okuyucuların temanın durumunu görme engelli kullanıcılara aktarabilmesi için simge düğmesinin durumunu tutacak bir
[aria-label]
ekleyin.
<button
class="theme-toggle"
id="theme-toggle"
title="Toggles light & dark"
aria-label="auto"
>
…
</button>
aria-label
ve aria-live
kibar
aria-label
'da yapılan değişikliklerin ekran okuyuculara 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 kullanıcıya neyin değiştiğini söyleme sinyali verir. Bu düğme, aria-label
'nın durumuna bağlı olarak "açık" veya "koyu" şeklinde 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şimde bulunmak vektörler için yeni görsel durumları tetikleyebilir. Bu nedenle SVG, simgeler için idealdir.
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, öğe sunum amaçlı olarak işaretlendiği için onu yoksayacaklarını anlıyor. Bu, düğme içindeki simge gibi görsel süslemeler için idealdir. Öğede gerekli olan 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 şekiller için uygun olan bir daire ve çizgilerden oluşur. <circle>
, cx
ve cy
özellikleri görüntü alanı boyutunun (24) yarısı olan 12 olarak ayarlanarak ortalanır. Ardından, boyutu ayarlayan 6
değerinde bir 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 özelliği bir SVG öğesinin
kimliğine işaret eder.
Bu kimliği bir sonraki adımda oluşturacaksınız. Son olarak, currentColor
ile sayfanın metin rengine uygun bir dolgu rengi verilir.
Güneş ışınları
Ardından, güneş ışığı çizgileri dairenin hemen altına, bir grup öğesi <g>
grubu içine 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ğ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şturuyor.
Ay
Işık (güneş) ile karanlık (ay) arasında kusursuz bir geçiş yanılsaması oluşturmak için ay, SVG maskesi kullanılarak güneş simgesinin büyütülmüş bir versiyonu olarak gösterilir.
<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 ile maskeler, beyaz ve siyah renklerin başka bir grafiğin bölümlerini kaldırmasına veya dahil etmesine olanak tanıyan güçlü bir araçtır. Güneş simgesi, bir daire şeklini maske alanına taşıyıp çıkararak SVG maskeli bir ay <circle>
şekliyle örtülür.
CSS yüklenmezse ne olur?

Sonucun çok büyük olmaması veya düzen sorunlarına neden olmaması için CSS yüklenmemiş gibi SVG'nizi test etmek iyi 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, ağ türbülansına karşı güzel savunma stilleri oluşturur.
Düzen
Tema anahtarı bileşeninin yüzey alanı küçüktür. Bu nedenle, düzen için ızgara veya esnek kutu kullanmanız gerekmez. Bunun yerine SVG konumlandırma ve CSS dönüştürmeleri kullanılır.
Stiller
.theme-toggle
stil
<button>
öğesi, simge şekilleri ve stillerinin kapsayıcısıdır. Bu üst bağlam, SVG'ye aktarılacak uyarlanabilir renkleri ve boyutları içerir.
İ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 imleç stili ekleyin. Hızlı tepki veren dokunma deneyimi için touch-action: manipulation
ekleyin.
iOS'in düğmelere uyguladığı yarı şeffaf vurguyu kaldırın. Son olarak, odak durumu ana hattına öğenin kenarından biraz boşluk 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ı vardır. SVG, düğmenin boyutuna uygun olmalı ve görsel olarak 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üğmesinin boyutu 2rem
ile biraz küçük. Bu, fare kullanıcıları için sorun olmasa da parmak gibi kaba bir işaretçi için zor olabilir. Boyut artışı belirtmek için fareyle üzerine gelme medya sorgusu kullanarak düğmenin birçok dokunma boyutu yönergesine uygun olması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 tutarken SVG, görsel ve animasyonlu yönlerini tutar. Bu bölümde simgeyi güzelleştirebilir ve canlandırabilirsiniz.
Açık tema

Ölçek ve döndürme animasyonlarının SVG şekillerinin merkezinden gerçekleşmesi için transform-origin: center center
değerini ayarlayın. Düğme tarafından sağlanan uyarlanabilir renkler burada şekiller tarafından kullanılır. Ay ve güneş, dolgu için var(--icon-fill)
ve var(--icon-fill-hover)
düğmesini kullanırken güneş ışınları, fırça 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 stillerinde güneş ışınları kaldırılmalı, güneş çemberi büyütülmeli ve çember maskesi taşınmalıdır.
.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ı fark edeceksiniz. Üst düğme bileşeni, renklerin sahibidir. Bu renkler, koyu ve açık bağlamda zaten uyarlanabilir. 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şler olmamalıdır. Aşağıdaki bölümler, geçişlerin nasıl ve ne şekilde tanımlanacağıyla ilgilidir.
Medya sorgularını paylaşma ve yumuşatma işlevlerini içe aktarma
Geçişleri ve animasyonları kullanıcının işletim sistemi hareket tercihlerinin arkasına yerleştirmeyi kolaylaştırmak için PostCSS eklentisi CustomMedia, 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 yumuşatma efektleri 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, ay geçişlerine kıyasla daha eğlenceli olacak ve bu efekt, esnek yumuşatma işlevleriyle sağlanacak. Güneş ışınları dönerken az miktarda yansımalı ve güneşin merkezi ölçeklenirken az miktarda yansımalı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, öğelerin ve kolaylaştırma zamanlamasının süresi incelenebilir.


Ay
Ayın ışıklı ve karanlık konumları zaten ayarlanmış durumda. Kullanıcının hareket tercihlerine saygı duyarak bu görünümü canlandırmak için --motionOK
medya sorgusuna geçiş stilleri ekleyin.
Bu geçişin sorunsuz olması için zamanlama, gecikme ve süre çok önemlidir. Örneğin, güneş çok erken tutulursa geçiş düzenli veya eğlenceli değil, kaotik görünür.
.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ş hareketi tercih etme
Çoğu GUI Challenge'da (GUI Yarışması) hareket azaltmayı tercih eden kullanıcılar için opaklık geçişleri gibi bazı animasyonları kullanmaya çalışıyorum. Ancak bu bileşen, anlık durum değişiklikleriyle daha iyi çalışıyordu.
JavaScript
Bu bileşende, ekran okuyucular için ARIA bilgilerini yönetmekten local storage'dan değerleri alıp ayarlamaya kadar JavaScript'in yapması gereken çok iş var.
Sayfa yükleme deneyimi
Sayfa yüklenirken renk yanıp sönmesinin olmaması önemliydi. Koyu renk şeması kullanan bir kullanıcı bu bileşenle açık temayı tercih ettiğini belirtip sayfayı yeniden yüklediğinde sayfa önce koyu renkte görünür, ardından açık renkte yanıp söner.
Bunu önlemek için, HTML özelliği data-theme
'yı mümkün olduğunca erken ayarlamak amacıyla az miktarda engelleme JavaScript'i çalıştırmak gerekiyordu.
<script src="./theme-toggle.js"></script>
Bunu sağlamak için, dokümandaki düz bir <script>
etiketi, CSS veya <body>
biçimlendirmesinden önce yüklenir.<head>
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ı dikkatli bir şekilde kullanarak, ana CSS sayfayı boyamadan önce HTML özelliğini ayarlayabilir, böylece bir flaş veya renk oluşmasını önleyebilirsiniz.
JavaScript, önce kullanıcının yerel depolamadaki tercihini kontrol eder ve depolamada hiçbir şey bulunamazsa sistem tercihini kontrol etmek için geri dönüş yapar:
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ı tercihlere göre değiştirecek bir işlev.
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 dokümanı 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
(diğer adıyla <html>
etiketi) bulunur. İşlev, her ikisini de senkronize tutmak için 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 söz dizimi hatası olmamasını ve setAttribute işlevinin çağrılmaya çalışılmasını sağlar.
Ardından, bu işlev reflectPreference()
hemen çağrılır. Böylece HTML belgesinin data-theme
özelliği ayarlanır:
reflectPreference()
Düğme için yine de özellik gerekir. Bu nedenle, sayfa yükleme etkinliğini bekleyin. Ardından, aşağıdakilerde sorgu oluşturmak, işleyici eklemek ve özellikleri ayarlamak güvenli olur:
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 tema, JavaScript belleğinde ve dokümanda değiştirilmelidir. Mevcut tema değerinin incelenmesi ve yeni durumuyla ilgili bir karar verilmesi gerekir. Yeni durum ayarlandıktan sonra kaydedin ve belgeyi güncelleyin:
const onClick = () => {
theme.value = theme.value === 'light'
? 'dark'
: 'light'
setPreference()
}
Sistemle senkronizasyon
Bu tema geçişine özgü olan özellik, sistem tercihi değiştikçe sistem tercihiyle senkronize edilmesidir. Bir kullanıcı, sayfa ve bu bileşen görünürken sistem tercihini değiştirirse tema anahtarı, kullanıcının sistem anahtarıyla aynı anda tema anahtarıyla etkileşime girmiş gibi yeni kullanıcı tercihine uyacak şekilde değişir.
Bunu JavaScript ve bir
matchMedia
medya sorgusunda yapılan değişiklikleri dinleyen bir etkinlik ile 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 nasıl yapardınız? 🙂
Yaklaşımlarımızı çeşitlendirelim ve web'de içerik oluşturmanın tüm yollarını öğrenelim. Bir demo oluşturun, bağlantıları bana tweet atın. Ben de bu bağlantıları aşağıdaki topluluk remiksleri bölümüne ekleyeyim.
Topluluk remiksleri
- Codepen with Vue'da @NathanG
- Codepen'de @ShadowShahriar
- Özel öğe olarak @tomayac
- vanilla JavaScript ile @bramus
- react ile @JoshWComeau