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

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.

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

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.

Şemada, temayı ayarlamak için 4 yol olduğunu genel olarak göstermek amacıyla JavaScript sayfa yükleme ve doküman etkileşim etkinliklerinin önizlemesi gösterilmektedir

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ş ışınlarının sönmesiyle gösterilen güneş simgesi ve ortadaki çemberi işaret eden
  sıcak pembe ok.

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ı

Güneş merkezi sönmüş şekilde gösterilen güneş simgesi ve güneş ışınlarını işaret eden
  sıcak pembe ok.

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>
Maskelemenin nasıl çalıştığını göstermeye yardımcı olan üç dikey katmanlı grafik. Üst katman, siyah bir dairenin bulunduğu beyaz bir karedir. Orta katman güneş simgesidir.
Alt katman, sonuç olarak etiketlenir ve üst katmandaki siyah dairenin bulunduğu yerde kesikli güneş simgesini gösterir.

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?

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

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

ALT_TEXT_HERE

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

ALT_TEXT_HERE

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.

Açıktan koyuya geçiş
Koyudan açık renge geçiş

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

İ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()
  })
MacOS sistem tercihini değiştirmek, tema geçiş durumunu değiştirir

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