Kavşak Gözlemcisi ile Dinamik Bir Başlık Oluşturma

Yayınlanan: 2022-03-10
Hızlı özet ↬ Sayfadaki bazı bileşenlerin, öğeler görünüm alanı içinde belirli bir eşiğe kaydırılırken bunlara yanıt vermesi gereken bir kullanıcı arayüzü oluşturmaya hiç ihtiyaç duydunuz mu? JavaScript'te, kaydırma sırasında sürekli olarak bir geri arama başlatmak için bir olay dinleyicisi eklemek performans açısından yoğun olabilir ve akıllıca kullanılmadığında ağır bir kullanıcı deneyimine neden olabilir. Ancak Kavşak Gözlemcisi ile daha iyi bir yol var.

Intersection Observer API, bir öğeyi gözlemlememizi ve kayan bir kapsayıcıda belirli bir noktadan - genellikle (ancak her zaman değil) görünüm alanından - bir geri arama işlevini tetikleyerek ne zaman geçtiğini algılamamızı sağlayan bir JavaScript API'sidir.

Kesişim Gözlemcisi, asenkron olduğu için ana iş parçacığındaki kaydırma olaylarını dinlemekten daha performanslı olarak kabul edilebilir ve geri arama, kaydırma konumu her güncellendiğinde bunun yerine gözlemlediğimiz öğe belirtilen eşiği karşıladığında yalnızca tetiklenir. Bu makalede, web sayfasının farklı bölümleriyle kesiştiğinde değişen sabit bir başlık bileşeni oluşturmak için Kesişme Gözlemcisi'ni nasıl kullanabileceğimize dair bir örnek üzerinden gideceğiz.

Temel Kullanım

Kesişme Gözlemcisi'ni kullanmak için, önce iki parametre alan yeni bir gözlemci oluşturmamız gerekir: Gözlemci seçeneklerine sahip bir nesne ve gözlemlediğimiz öğe (gözlemci hedefi olarak bilinir) kesiştiğinde yürütmek istediğimiz geri çağırma işlevi kök ile (hedef öğenin atası olması gereken kayan kapsayıcı).

 const options = { root: document.querySelector('[data-scroll-root]'), rootMargin: '0px', threshold: 1.0 } const callback = (entries, observer) => { entries.forEach((entry) => console.log(entry)) } const observer = new IntersectionObserver(callback, options)

Gözlemcimizi oluşturduğumuzda, ona bir hedef elemanı izlemesi talimatını vermeliyiz:

 const targetEl = document.querySelector('[data-target]') observer.observe(targetEl)

Varsayılan değerlerine geri döneceğinden, seçenekler değerlerinden herhangi biri atlanabilir:

 const options = { rootMargin: '0px', threshold: 1.0 }

Kök belirtilmezse, tarayıcı görünüm alanı olarak sınıflandırılır. Yukarıdaki kod örneği, hem rootMargin hem de threshold için varsayılan değerleri gösterir. Bunları görselleştirmek zor olabilir, bu nedenle açıklamaya değer:

rootMargin

rootMargin değeri, kök öğeye CSS kenar boşlukları eklemeye benzer ve tıpkı kenar boşlukları gibi, negatif değerler de dahil olmak üzere birden çok değer alabilir. Hedef öğenin kenar boşluklarına göre kesiştiği kabul edilecektir.

Pozitif ve negatif kök kenar boşluğu değerlerine sahip kaydırma kökü. Turuncu kare, varsayılan eşik değeri 1 olarak kabul edilerek "kesişen" olarak sınıflandırılacağı noktada konumlandırılır (Büyük önizleme)

Bu, bir öğenin teknik olarak görüş alanı dışındayken bile "kesişen" olarak sınıflandırılabileceği anlamına gelir (eğer kaydırma kökümüz görünüm alanıysa).

Turuncu kare, görünür alanın dışında olmasına rağmen kökle kesişiyor. (Büyük önizleme)

rootMargin varsayılan olarak 0px , ancak tıpkı CSS'deki margin özelliğini kullanmak gibi, birden çok değerden oluşan bir dize alabilir.

threshold

threshold , tek bir değerden veya 0 ile 1 arasında bir dizi değerden oluşabilir. Kesişen olarak kabul edilmesi için kök sınırları içinde olması gereken öğenin oranını temsil eder. 1 varsayılan değerini kullanarak, hedef öğenin %100'ü kök içinde görünür olduğunda geri arama başlatılır.

Sırasıyla 1, 0 ve 0,5'lik eşikler, %100, %0 ve %50 görünür olduğunda geri arama tetiklenmesine neden olur. (Büyük önizleme)

Bu seçenekler kullanılarak bir öğenin ne zaman görünür olarak sınıflandırılacağını görselleştirmek her zaman kolay değildir. Kavşak Gözlemcisi ile başa çıkmanıza yardımcı olacak küçük bir araç geliştirdim.

Atlamadan sonra daha fazlası! Aşağıdan okumaya devam edin ↓

Başlık Oluşturma

Artık temel ilkeleri kavradığımıza göre, dinamik başlığımızı oluşturmaya başlayalım. Bölümlere ayrılmış bir web sayfasıyla başlayacağız. Bu resim, oluşturacağımız sayfanın tam düzenini göstermektedir:

(Büyük önizleme)

Bu makalenin sonuna bir demo ekledim, bu yüzden kodun seçimini kaldırmak istiyorsanız doğrudan ona atlamaktan çekinmeyin. (Ayrıca bir Github deposu da var.)

Her bölümün minimum yüksekliği 100vh (içeriğe bağlı olarak daha uzun olabilir). Başlığımız sayfanın en üstüne sabitlenir ve kullanıcı sayfayı kaydırdıkça yerinde kalır ( position: fixed kullanılarak). Bölümlerin farklı renkli arka planları vardır ve başlıkla karşılaştıklarında başlığın renkleri bölümün renklerini tamamlamak için değişir. Ayrıca, kullanıcının içinde bulunduğu bölümü gösteren ve bir sonraki bölüm geldiğinde kayan bir işaretçi vardır. Doğrudan ilgili koda ulaşmamızı kolaylaştırmak için, devam etmek isterseniz, başlangıç ​​noktamızla (Kesişim Gözlemcisi API'sini kullanmaya başlamadan önce) minimum bir demo oluşturdum.

işaretleme

Başlığımız için HTML ile başlayacağız. Bu, bir ana sayfa bağlantısı ve gezinme ile oldukça basit bir başlık olacak, özellikle süslü bir şey değil, ancak birkaç veri özniteliği kullanacağız: başlığın kendisi için data-header başlığı (böylece öğeyi JS ile hedefleyebiliriz) ve tıklandığında kullanıcıyı ilgili bölüme kaydıracak data-link özelliğine sahip üç bağlantı bağlantısı:

 <header data-header> <nav class="header__nav"> <div class="header__left-content"> <a href="#0">Home</a> </div> <ul class="header__list"> <li> <a href="#about-us" data-link>About us</a> </li> <li> <a href="#flavours" data-link>The flavours</a> </li> <li> <a href="#get-in-touch" data-link>Get in touch</a> </li> </ul> </nav> </header>

Ardından, sayfamızın geri kalanı için bölümlere ayrılmış HTML. Kısaca, yalnızca makaleyle ilgili kısımları ekledim, ancak tam biçimlendirme demoda yer alıyor. Her bölüm, arka plan renginin adını belirten bir veri özniteliği ve başlıktaki bağlantı bağlantılarından birine karşılık gelen bir id içerir:

 <main> <section data-section="raspberry"> <!--Section content--> </section> <section data-section="mint"> <!--Section content--> </section> <section data-section="vanilla"> <!--Section content--> </section> <section data-section="chocolate"> <!--Section content--> </section> </main>

Başlığımızı CSS ile konumlandıracağız, böylece kullanıcı kaydırdıkça sayfanın en üstünde sabit kalacak:

 header { position: fixed; width: 100%; }

Ayrıca bölümlerimize minimum bir yükseklik vereceğiz ve içeriği ortalayacağız. (Bu kod Kavşak Gözlemcisinin çalışması için gerekli değildir, sadece tasarım içindir.)

 section { padding: 5rem 0; min-height: 100vh; display: flex; justify-content: center; align-items: center; }

iframe Uyarı

Bu Codepen demosunu oluştururken, mükemmel çalışması gereken Intersection Observer kodumun, kavşağın doğru noktasında geri aramayı başlatmaması, bunun yerine hedef eleman görüntü alanı kenarıyla kesiştiğinde ateşlenmesi gibi kafa karıştırıcı bir sorunla karşılaştım. Biraz kafa karıştırdıktan sonra, bunun Codepen'de içeriğin farklı şekilde ele alınan bir iframe içinde yüklenmesinden kaynaklandığını anladım. (Tüm ayrıntılar için MDN belgelerinin Kırpma ve kesişme dikdörtgeni hakkındaki bölümüne bakın.)

Geçici bir çözüm olarak, demoda işaretlememizi, beklediğimiz gibi tarayıcı görünüm alanı yerine kaydırma kabı (IO seçeneklerimizdeki kök) olarak işlev görecek başka bir öğeye sarabiliriz:

 <div class="scroller" data-scroller> <header data-header> <!--Header content--> </header> <main> <!--Sections--> </main> </div>

Aynı demo için görünüm alanını kök olarak nasıl kullanacağınızı görmek istiyorsanız, bu Github deposuna dahil edilmiştir.

CSS

CSS'mizde, kullandığımız renkler için bazı özel özellikler tanımlayacağız. Ayrıca başlık metni ve arka plan renkleri için iki ek özel özellik tanımlayacağız ve bazı başlangıç ​​değerleri ayarlayacağız. (Bu iki özel özelliği farklı bölümler için daha sonra güncelleyeceğiz.)

 :root { --mint: #5ae8d5; --chocolate: #573e31; --raspberry: #f2308e; --vanilla: #faf2c8; --headerText: var(--vanilla); --headerBg: var(--raspberry); }

Bu özel özellikleri başlığımızda kullanacağız:

 header { background-color: var(--headerBg); color: var(--headerText); }

Ayrıca farklı bölümlerimiz için renkleri ayarlayacağız. Veri özniteliklerini seçiciler olarak kullanıyorum, ancak isterseniz bir sınıfı da kolayca kullanabilirsiniz.

 [data-section="raspberry"] { background-color: var(--raspberry); color: var(--vanilla); } [data-section="mint"] { background-color: var(--mint); color: var(--chocolate); } [data-section="vanilla"] { background-color: var(--vanilla); color: var(--chocolate); } [data-section="chocolate"] { background-color: var(--chocolate); color: var(--vanilla); }

Ayrıca her bölüm görüntülendiğinde başlığımız için bazı stiller ayarlayabiliriz:

 /* Header */ [data-theme="raspberry"] { --headerText: var(--raspberry); --headerBg: var(--vanilla); } [data-theme="mint"] { --headerText: var(--mint); --headerBg: var(--chocolate); } [data-theme="chocolate"] { --headerText: var(--chocolate); --headerBg: var(--vanilla); }

Burada veri özniteliklerini kullanmak için daha güçlü bir durum var çünkü her kesişmede başlığın data-theme özniteliğini değiştireceğiz.

Gözlemci Oluşturma

Artık sayfa kurulumumuz için temel HTML ve CSS'ye sahip olduğumuza göre, görüntülenen her bir bölümümüzü izlemek için bir gözlemci oluşturabiliriz. Sayfayı aşağı kaydırırken bir bölüm başlığın alt kısmıyla temas ettiğinde bir geri arama başlatmak istiyoruz. Bu, başlığın yüksekliğine karşılık gelen bir negatif kök marjı ayarlamamız gerektiği anlamına gelir.

 const header = document.querySelector('[data-header]') const sections = [...document.querySelectorAll('[data-section]')] const scrollRoot = document.querySelector('[data-scroller]') const options = { root: scrollRoot, rootMargin: `${header.offsetHeight * -1}px`, threshold: 0 }

Bölümün herhangi bir kısmı kök kenar boşluğu ile kesişiyorsa tetiklenmesini istediğimiz için 0 eşiğini ayarlıyoruz.

Her şeyden önce, başlığın data-theme değerini değiştirmek için bir geri arama oluşturacağız. (Bu, özellikle başlık elemanımızda başka sınıfların uygulanmış olabileceği durumlarda, sınıfları ekleyip kaldırmaktan daha kolaydır.)

 /* The callback that will fire on intersection */ const onIntersect = (entries) => { entries.forEach((entry) => { const theme = entry.target.dataset.section header.setAttribute('data-theme', theme) }) }

Ardından kesişen bölümleri izlemek için gözlemci oluşturacağız:

 /* Create the observer */ const observer = new IntersectionObserver(onIntersect, options) /* Set our observer to observe each section */ sections.forEach((section) => { observer.observe(section) })

Şimdi her bölüm başlıkla karşılaştığında başlık renklerimizin güncellendiğini görmeliyiz.

Michelle Barker'ın kalemine [Mutlu Yüzlü Dondurma Salonu – Adım 2](https://codepen.io/smashingmag/pen/poPgpjZ) bakın.

Michelle Barker'ın kaleminden Happy Face Dondurma Salonu - Adım 2'yi görün.

Ancak, aşağı kaydırırken renklerin doğru şekilde güncellenmediğini fark edebilirsiniz. Aslında, başlık her seferinde önceki bölümün renkleriyle güncelleniyor! Yukarı doğru kaydırmak ise mükemmel çalışıyor. Kaydırma yönünü belirlemeli ve buna göre davranışı değiştirmeliyiz.

Kaydırma Yönünü Bulma

JS'mizde kaydırma yönü için, başlangıç ​​değeri 'up' olan bir değişken ve bilinen son kaydırma konumu için başka bir değişken ayarlayacağız ( prevYPosition ). Ardından, geri çağırma içinde, kaydırma konumu bir önceki değerden büyükse, direction değerini 'down' veya tersi ise 'up' olarak ayarlayabiliriz.

 let direction = 'up' let prevYPosition = 0 const setScrollDirection = () => { if (scrollRoot.scrollTop > prevYPosition) { direction = 'down' } else { direction = 'up' } prevYPosition = scrollRoot.scrollTop } const onIntersect = (entries, observer) => { entries.forEach((entry) => { setScrollDirection() /* ... */ }) }

Ayrıca başlık renklerini güncellemek için yeni bir işlev oluşturacağız ve hedef bölümü bir argüman olarak ileteceğiz:

 const updateColors = (target) => { const theme = target.dataset.section header.setAttribute('data-theme', theme) } const onIntersect = (entries) => { entries.forEach((entry) => { setScrollDirection() updateColors(entry.target) }) }

Şimdiye kadar başlığımızın davranışında bir değişiklik görmemeliyiz. Ancak artık kaydırma yönünü bildiğimize göre, updateColors() işlevimiz için farklı bir hedefe geçebiliriz. Kaydırma yönü yukarıysa, giriş hedefini kullanırız. Eğer çalışmıyorsa, bir sonraki bölümü kullanacağız (eğer varsa).

 const getTargetSection = (target) => { if (direction === 'up') return target if (target.nextElementSibling) { return target.nextElementSibling } else { return target } } const onIntersect = (entries) => { entries.forEach((entry) => { setScrollDirection() const target = getTargetSection(entry.target) updateColors(target) }) }

Ancak bir sorun daha var: başlık, yalnızca bölüm başlığa ulaştığında değil, bir sonraki öğe görünümün altında görüntülendiğinde de güncellenecektir. Bunun nedeni, gözlemcimizin geri aramayı iki kez başlatmasıdır: bir kez eleman girerken ve tekrar çıkarken.

Başlığın güncellenip güncellenmeyeceğini belirlemek için entry nesnesinden isIntersecting anahtarını kullanabiliriz. Başlık renklerinin güncellenmesi gerekip gerekmediğine ilişkin bir boole değeri döndürmek için başka bir işlev oluşturalım:

 const shouldUpdate = (entry) => { if (direction === 'down' && !entry.isIntersecting) { return true } if (direction === 'up' && entry.isIntersecting) { return true } return false }

onIntersect() işlevimizi buna göre güncelleyeceğiz:

 const onIntersect = (entries) => { entries.forEach((entry) => { setScrollDirection() /* Do nothing if no need to update */ if (!shouldUpdate(entry)) return const target = getTargetSection(entry.target) updateColors(target) }) }

Artık renklerimiz doğru şekilde güncellenmelidir. Efektin biraz daha güzel olması için bir CSS geçişi ayarlayabiliriz:

 header { transition: background-color 200ms, color 200ms; } 

Michelle Barker'ın kaleminden [Mutlu Yüzlü Dondurma Salonu – 3. Adım](https://codepen.io/smashingmag/pen/bGWEaEa) bakın.

Michelle Barker'ın kaleminden Happy Face Dondurma Salonu - Adım 3'ü görün.

Dinamik İşaretçiyi Ekleme

Ardından, farklı bölümlere kaydırdıkça konumunu güncelleyen başlığa bir işaret ekleyeceğiz. Bunun için bir sözde eleman kullanabiliriz, bu yüzden HTML'mize herhangi bir şey eklememize gerek yok. Onu başlığın sol üst köşesine yerleştirmek için basit bir CSS stili vereceğiz ve ona bir arka plan rengi vereceğiz. Başlık metni renginin değerini alacağı için bunun için currentColor kullanıyoruz:

 header::after { content: ''; position: absolute; top: 0; left: 0; height: 0.4rem; background-color: currentColor; }

Genişlik için varsayılan değeri 0 olan özel bir özellik kullanabiliriz. Ayrıca, çevirme x değeri için özel bir özellik kullanacağız. Kullanıcı kaydırdıkça geri arama işlevimizde bunlar için değerleri ayarlayacağız.

 header::after { content: ''; position: absolute; top: 0; left: 0; height: 0.4rem; width: var(--markerWidth, 0); background-color: currentColor; transform: translate3d(var(--markerLeft, 0), 0, 0); }

Şimdi işaretçinin kesişim noktasındaki genişliğini ve konumunu güncelleyecek bir fonksiyon yazabiliriz:

 const updateMarker = (target) => { const id = target.id /* Do nothing if no target ID */ if (!id) return /* Find the corresponding nav link, or use the first one */ let link = headerLinks.find((el) => { return el.getAttribute('href') === `#${id}` }) link = link || headerLinks[0] /* Get the values and set the custom properties */ const distanceFromLeft = link.getBoundingClientRect().left header.style.setProperty('--markerWidth', `${link.clientWidth}px`) header.style.setProperty('--markerLeft', `${distanceFromLeft}px`) }

Renkleri güncellerken aynı zamanda işlevi çağırabiliriz:

 const onIntersect = (entries) => { entries.forEach((entry) => { setScrollDirection() if (!shouldUpdate(entry)) return const target = getTargetSection(entry.target) updateColors(target) updateMarker(target) }) }

Ayrıca işaretçi için bir başlangıç ​​konumu belirlememiz gerekecek, böylece birdenbire ortaya çıkmaz. Belge yüklendiğinde, ilk bölümü hedef olarak kullanarak updateMarker() işlevini çağıracağız:

 document.addEventListener('readystatechange', e => { if (e.target.readyState === 'complete') { updateMarker(sections[0]) } })

Son olarak, işaretçinin başlık boyunca bir bağlantıdan diğerine kayması için bir CSS geçişi ekleyelim. width özelliğini değiştirirken, tarayıcının optimizasyon gerçekleştirmesini sağlamak için will-change kullanabiliriz.

 header::after { transition: transform 250ms, width 200ms, background-color 200ms; will-change: width; }

Düzgün Kaydırma

Son bir dokunuş olarak, bir kullanıcı bir bağlantıyı tıkladığında, bölüme atlamak yerine sayfada sorunsuzca aşağı kaydırılırsa iyi olur. Bugünlerde bunu CSS'mizde yapabiliyoruz, JS'ye gerek yok! Daha erişilebilir bir deneyim için, sistem ayarlarında azaltılmış hareket için bir tercih belirtmemişlerse, yalnızca yumuşak kaydırma uygulayarak kullanıcının hareket tercihlerine saygı göstermek iyi bir fikirdir:

 @media (prefers-reduced-motion: no-preference) { .scroller { scroll-behavior: smooth; } }

Son Demo

Yukarıdaki tüm adımları bir araya getirmek, eksiksiz bir demo ile sonuçlanır.

Michelle Barker'ın kaleminden [Mutlu Yüzlü Dondurma Salonu – Kavşak Gözlemcisi örneği](https://codepen.io/smashingmag/pen/XWRXVXQ) bakın.

Michelle Barker'ın Kalem Mutlu Yüz Dondurma Salonu – Kavşak Gözlemcisi örneğine bakın.

Tarayıcı Desteği

Intersection Observer, modern tarayıcılarda yaygın olarak desteklenmektedir. Gerektiğinde eski tarayıcılar için çoklu doldurulabilir - ancak mümkün olduğunda aşamalı bir geliştirme yaklaşımını tercih ederim. Başlığımız söz konusu olduğunda, desteklenmeyen tarayıcılar için basit, değişmeyen bir sürüm sağlamak kullanıcı deneyimine büyük ölçüde zarar vermez.

Intersection Observer'ın desteklenip desteklenmediğini tespit etmek için aşağıdakileri kullanabiliriz:

 if ('IntersectionObserver' in window && 'IntersectionObserverEntry' in window && 'intersectionRatio' in window.IntersectionObserverEntry.prototype) { /* Code to execute if IO is supported */ } else { /* Code to execute if not supported */ }

Kaynaklar

Kavşak Gözlemcisi hakkında daha fazla bilgi edinin:

  • MDN'den bazı pratik örneklerle birlikte kapsamlı belgeler
  • Kavşak Gözlemcisi görselleştirici aracı
  • Intersection Observer API ile Zamanlama Öğesi Görünürlüğü – MDN'den reklam görünürlüğünü izlemek için IO'nun nasıl kullanılabileceğine bakan başka bir eğitici
  • Denys Mishunov'un bu makalesi, tembel yüklenen varlıklar da dahil olmak üzere IO'nun diğer bazı kullanımlarını kapsar. Bu artık daha az gerekli olsa da ( loading özelliği sayesinde), burada öğrenilecek çok şey var.