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

Uyarlanabilir ve erişilebilir tema anahtarı bileşeni oluşturmaya dair temel bir genel bakış.

Bu yayında, koyu ve açık tema anahtarı bileşeni oluşturma konusundaki düşüncelerimi paylaşmak istiyorum. Demoyu deneyin.

Demo düğmesinin boyutu, kolayca görülebilmesi 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 dikkate alınması gereken birkaç web mühendisliği hususu 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 ile ilgili değişikliklerin duyurulmasını 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 kullanıcıya neyin değiştiğini aria-live="assertive" yerine kibarca söylemelerini işaret eder. Bu düğme, aria-label değerinin ne olduğuna bağlı olarak "açık" veya "koyu" olarak 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ş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 eklendi. Böylece ekran okuyucular, sunumu belirtmek için işaretlendiğinden öğ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ı soluklaştırılmış ve ortada daireyi gösteren parlak pembe bir okla gösterilen güneş simgesi.

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ş simgesi, ortası karartılmış ve güneş ışınlarını gösteren parlak pembe bir okla gösterilir.

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 daire içeren 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ü.

SVG'nizi, sonucun çok büyük olmadığından veya sayfa düzeni sorunlarına neden olmadığından emin olmak için 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ının kullanabileceği 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ırma ve CSS dönüştürme işlemleri kullanılır.

Stiller

.theme-toggle stiller

<button> öğesi, simge şekilleri ve stillerinin kapsayıcısıdır. Bu üst bağlam, SVG'ye aktarılacak uyarlanabilir renkleri ve boyutları barındırır.

İlk olarak düğmeyi daire şeklinde yapın ve varsayılan düğme stillerini kaldırı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%;
}

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 uzak olmasına dikkat edin:

.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ışını belirtmek için fareyle üzerine gelme medya sorgusu kullanarak düğmenin birçok dokunma boyutu kuralına uyması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 içerirken içindeki SVG, görsel ve animasyonlu yönleri içerir. Bu aşamada simge güzelleştirilebilir ve canlandırılabilir.

Açık tema

ALT_TEXT_HERE

Ölçeklendirme ve döndürme animasyonlarının SVG şekillerinin ortasından gerçekleşmesi için transform-origin: center center ayarlarını 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 duruma duyarlı olmalıdır ancak bu aşamada geçişler olmamalıdır. 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 Özel Medya, geçiş ve animasyonları kullanıcının işletim sistemi hareket tercihlerine göre ayarlamanızı kolaylaştırmak için medya sorgusu değişkenleri için taslak CSS spesifikasyonunun söz dizimini kullanmanıza olanak tanır:

@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, akıcı geçişlerle bu efekti elde ederek aya kıyasla daha eğlenceli olur. 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 karanlık konumları zaten ayarlanmıştır. Kullanıcının hareket tercihlerine saygı duyarken bu öğeyi canlandırmak 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çıkta 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ını ölçülü bir şekilde kullanarak HTML özelliğini ana CSS sayfayı boyamadan önce ayarlayabilir, böylece parlama veya renklerin gösterilmesini ö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, JavaScript belleğinde ve belgede temanın değiştirilmesi gerekir. Mevcut tema değerinin incelenmesi ve yeni durumu hakkında karar verilmesi gerekir. Yeni durumu ayarladıktan sonra kaydedin ve belgeyi 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 tercihi değiştirildiğinde tema anahtarı durumu da değişir

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