Tema değiştirme bileşeni oluşturma

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.

Demo düğmesinin boyutu, kolay görünürlük için artırıldı

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.

Şemada, JavaScript sayfa yükleme ve doküman etkileşimi etkinliklerinin önizlemesi gösterilerek temayı ayarlamak için 4 yol olduğu belirtiliyor.

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ş ışınları soluk ve ortadaki daireyi işaret eden pembe bir okla gösterilen güneş simgesi.

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ı

Güneş simgesinin ortası soluk ve güneş ışınlarını gösteren pembe bir okla gösteriliyor.

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>
Maskelemenin nasıl çalıştığını göstermeye yardımcı olan üç dikey katmanlı grafik. En üstteki katman, siyah daireli beyaz bir karedir. Ortadaki katman güneş simgesidir.
En alttaki katman sonuç olarak etiketlenir ve üst katmandaki siyah dairenin kesildiği yerde güneş simgesi gösterilir.

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?

İçinde güneş simgesi bulunan düz bir tarayıcı düğmesinin ekran görüntüsü.

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

ALT_TEXT_HERE

Ö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

ALT_TEXT_HERE

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.

Açıktan koyuya geçiş
Koyudan açığa geçiş

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;
      }
    }
  }
}
Açıktan koyuya geçiş
Koyudan açığa geçiş

İ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()
  })
MacOS sistem tercihini değiştirmek tema anahtarının durumunu değiştirir.

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