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

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.

demo düğmesinin boyutu 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 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.

Şema, JavaScript sayfa yükleme ve belge etkileşim etkinliklerinin önizlemesini, genel olarak, temayı ayarlamak için 4 yol olduğunu göstermek üzere gösterir.

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ş

Sönmüş güneş ışınlarıyla birlikte gösterilen güneş simgesi ve ortadaki daireyi gösteren hotpink ok.

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

Güneş merkezi soluk renkte gösterilen güneş simgesi ve güneş ışınlarını gösteren hotpink ok.

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>
Maskelemenin nasıl çalıştığını göstermeye yardımcı olan üç dikey katman içeren grafik. Üst katman, siyah dairesi olan beyaz bir karedir. Ortadaki katman güneş simgesidir.
Alt katman sonuç olarak etiketlenmiştir ve üst katman siyah dairenin bulunduğu yerde kesik bir kesikle birlikte güneş simgesi gösterilmektedir.

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?

İçinde güneş simgesi olan 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 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

ALT_TEXT_HERE

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

ALT_TEXT_HERE

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.

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

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

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

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