Gelişmiş Ağaç Sayma: Sibling-index() ve sibling-count() ile Matematiksel Düzenler
Twitter'da paylaş, LinkedIn
sibling-index() ve sibling-count() ile tanışın. :nth-child() kuralları veya JS geçici çözümleri olmadan CSS'nin bir satırında kademeli basamaklı efekt. 5 ürün veya 5.000 için çalışır. Meetand. Kurallar veya JS geçici çözümleri olmadan tek bir CSS satırında kademeli basamaklı efekt. 5 ürün veya 5.000 için çalışır.
Nasıl Önlem Alınmalı?
Kartlardan oluşan bir ızgaraya sahip olduğunuz ve onların birbiri ardına kaybolmasını istediğiniz şeyi biliyor musunuz? Bu şaşırtıcı çağlayan etkisi. Harika görünüyor. Basit olmalı. Ve yine de onu her kurduğumda, uygulama bana temelde aptalca bir şey yapıyormuşum gibi hissettirdi.
Çünkü seçenekler hep aynıydı. 10 öğeden oluşan bir listede kademeli animasyon gecikmeleri istediğinizi varsayalım. Ya bir düzine :nth-child() kuralını yayan bir Sass döngüsü yazdınız, her biri söz konusu belirli konum için bir --index değişkenini kodladı:
/* Öğe başına bir kural. Umarım liste hiç büyümez. */ li:n'inci-çocuk(1) { --idx: 1; } li:n'inci-çocuk(2) { --idx: 2; } li:n'inci-çocuk(3) { --idx: 3; } /* ... bunlardan sekiz tane daha ... */ li:nth-child(10) { --idx: 10; } li { animasyon gecikmesi: calc(var(--idx) * 100ms); }
On öğe. On kural. Liste 50'ye çıkarsa? Sınırlandırır ve en iyisini umarsınız ya da derleme sırasında yüzlerce seçici üreten bir Sass döngüsü kurarsınız. Roman Komarov gibi mühendisler O(√N) stratejileri geliştirdiler - meşru olarak akıllıca şeyler - ama yine de 1.023 elementi kapsayan 63 kuralla karşılaşıyorsunuz.
Uzmanların Görüşleri
Veya JavaScript'teki öğeler arasında dolaşıp satır içi stilleri ayarladınız. stil = "--index: 3" . Tam orada, DOM'da. İyi çalışıyor. Ayrıca düzen endişelerini komut dosyalarınıza yayar ve altı ay sonra birisi CSS'nin JavaScript enjekte edilmiş bir değişkene bağlı olduğunu fark etmeden bileşeni yeniden düzenlediğinde sessizce bozulur.
Her iki yaklaşım da beni hep aynı nedenden dolayı rahatsız etti: tarayıcıya zaten bildiği bir şeyi söylüyorsunuz. Tarayıcı DOM ağacını oluşturdu. Üçüncü çocuğun hangi element olduğunu bilir. Verileri var. CSS buna erişemedi.
Gelecekte Ne Bekleniyor?
Artık şunları yapabilir:
li { animasyon gecikmesi: calc(kardeş-index() * 100ms); }
Önemli Gelişmeler
Bir satır. 5 ürün veya 5.000 için çalışır. Etkinlik dinleyicisi yok. Mutasyon gözlemcisi yok. Yeniden oluşturma yok.
sibling-index() ve sibling-count(), CSS Değerleri ve Birimler Modülü Seviye 5 spesifikasyonunun bir parçasıdır (W3C taslaklarını eğlence için okuyan türdeyseniz Bölüm 9). Teklif, kapsamlı tartışmaların ardından CSSWG'nin 4559 numaralı sayısı aracılığıyla onaylandı. İşlevlerin kendileri hiçbir argüman almaz; yalnızca onları kullanırsınız.
Teknik Analiz
sibling-index() size bir öğenin ebeveyninin çocukları arasındaki 1 tabanlı konumunu verir. İlk çocuk 1 döndürür. Beşinci çocuk 5 değerini döndürür. Yalnızca öğe düğümlerini sayar; metin düğümleri, yorumlar ve boşlukların tümü ona görünmez.
size bir öğenin ebeveyninin çocukları arasındaki 1 tabanlı konumunu verir. İlk çocuk geri döner. Beşinci çocuk geri döner. Yalnızca öğe düğümlerini sayar; metin düğümleri, yorumlar ve boşlukların tümü ona görünmez. sibling-count() size ebeveynin sahip olduğu toplam öğe çocuk sayısını verir. Temel olarak, JavaScript'teki element.parentElement.children.length öğesinin CSS eşdeğeri, ancak stil sayfanızda mevcut.
Her iki işlev de gerçek bir sayı olan
İnsanları şaşırtan bir açıklama: :nth-child() bir seçicidir. Öğeleri seçer. Bir değer üretmez. calc(:nth-child() * 10px) yazamazsınız; bu geçerli bir CSS değildir. sibling-index() tam tersini yapar. Beyanlarınızın içinde yer alır ve size hesaplayabileceğiniz bir sayı verir. Farklı sorunları çözüyorlar ve şu ana kadar :nth-child()'ı asla tasarlanmadığı bir role bantlıyorduk.
Detaylar ve Etkileri
Çalmaya Değer Desenler
Bunların yalnızca tam sayılar olduğu tıklandığında fikirler hızla gelir.
Sonuç ve Değerlendirme
Son öğenin ilk önce canlandırılmasını ister misiniz? Çıkar:
.card { animasyon: 0,4 saniyelik yavaş yavaş geçiş her ikisini de kolaylaştırır; animasyon gecikmesi: calc((kardeş-sayısı() - kardeş-index()) * 80ms); }
Sistem Güvenliği
Son çocuk (N - N) * 80ms = 0ms alır — anında etkinleşir. İlk çocuk (N - 1) * 80ms alır. Animasyon, garip bir ritim için duraklamak yerine sayfa yüklendiği anda başlıyor.
Otom Eşit Genişlikler
Yüzdeleri ayarlamak için çocukları manuel olarak saymayı bırakın:
.tab { genişlik: hesapla(100% / kardeş-sayısı()); }
Beş sekme mi? Her biri %20. Altıncıyı ekle? %16,66. İki tanesi kaldırılsın mı? %25. Medya sorgusu yok, yeniden boyutlandırma gözlemcisi yok, JavaScript yok.
Bununla birlikte, çok fazla öğenin gerçekten dar sekmeler oluşturduğu bir senaryo hayal edebilirsiniz; bu noktada başka bir şeyle, belki de bir Flexbox sarma çözümüyle gitmek isteyebilirsiniz.
Renkleri renk tekerleğine eşit şekilde dağıtın:
.swatch { arka plan rengi: hsl( calc((360deg / kardeş-sayısı()) * kardeş-index()) %70 %50 ); }
Üç öğenin renkleri birbirinden 120° ayrılıyor. On iki öğe 30°'lik artışlar alır. Palet, DOM'da bulunan her şeye uyum sağlar; bu, normalde bir JavaScript renk kitaplığının yapacağı türden bir şeydir.
Öğeleri bir daire içinde dağıtmak, JavaScript'te sinüs ve kosinüs hesaplamak anlamına geliyordu. CSS'de artık sin() ve cos() doğal olarak var (Juan Diego RodrÃguez'in CSS-Tricks'te bunların pratik bir açıklaması var) ve ağaç sayımıyla birleştirildiğinde her şey saf CSS'ye dönüşüyor:
.radyal-öğe { --açı: calc((360deg / kardeş-sayısı()) * kardeş-index()); --yarıçap: 120px; konum: mutlak; sol: calc(50% + var(--radius) * cos(var(--angle))); üst: calc(50% + var(--radius) * sin(var(--angle))); transform: döndür(calc(var(--angle) * -1)); }
Altı öğe mi? Altıgen. Sekiz? Sekizgen. Öğeleri eklediğinizde veya kaldırdığınızda düzen yeniden hesaplanır. JavaScript hesaplama koordinatları yok.
Bir kart hayranı mı oluşturuyorsunuz? Bir satır:
.card { z-index: calc(kardeş-sayısı() - kardeş-index()); }
İlk kart destesi en yüksek olur, son kart 0 alır. Tersini istiyorsanız matematiği çevirin.
Bunlar tek tek incelenmeye değer çünkü teknik özelliklerden açıkça anlaşılmıyorlar.
Gölge DOM Kapsamı Belirleme
sibling-index() ve sibling-count(), düzleştirilmiş görsel ağaçta değil, DOM ağacında çalışır. Web Components'ta bu ayrım kesinlikle sizi ısıracaktır.
Bu gölge DOM'a sahip özel bir öğeniz olduğunu varsayalım:
.internal'ı sibling-index() ile stillendirirseniz, 2 değerini döndürür. Her zaman.
Burada bir de güvenlik meselesi var. Hafif bir DOM stil sayfası ::part() aracılığıyla bir bileşene ulaşmaya çalışırsa ve sibling-index() işlevini kullanırsa tarayıcı 0 değerini döndürür. Düz sıfır. Harici CSS'nin üçüncü taraf bileşenlerin iç yapısını incelemesini önlemek için kasıtlı bir duvardır. Dürüst olmak gerekirse bunun doğru çağrı olduğunu düşünüyorum.
Sözde Unsurlar Sayılmaz
::önce ve ::sonra kardeş değildir. Sibling-count()'ta görünmezler ve kendi sibling-index()'leri yoktur. Ancak - ve bu sizi hata ayıklama oturumundan kurtaracak kısımdır - bu işlevleri sözde öğe bildirimlerinin içinde kullanabilirsiniz. #target::before { width: calc(sibling-index() * 10px); yazdığınızda } , sibling-index() öğesini sözde öğeye göre değil, #target öğesine göre değerlendirir. Sözde öğe gerçek bir düğüm değildir, bu nedenle işlev, kaynak öğeye kadar uzanır. ::slotted(*)::before ile aynı hikaye — slotlu öğenin hafif DOM'daki indeksini kontrol eder.
ekran: yok Hala Önemli
Bu beni yaktı. Gösterilen öğeler: hiçbiri düzen ağacından kaybolmaz. Hiç yer kaplamazlar. Ekran okuyucular bunları görmez. Ama hâlâ DOM'dalar.
sibling-index() düzeni ağacını değil DOM ağacını okuduğundan, gizli öğeler sayılır:
- Elma
- Kiraz
Kiraz 2 değil 3'tür. Saklı muz hâlâ yerini koruyor.
Çoğu düzen için bu önemli değildir. Ancak, display: none ile eşleşmeyen öğeleri gizleyen bir arama filtresi gibi bir şey oluşturuyorsanız, kademeli animasyonlarınız ve dairesel düzenleriniz boşluklar oluşturacaktır. Görünür öğeler orijinal, sıralı olmayan dizinlerini korur. Sürekli saymaya bağlı olan herhangi bir şey için (radyal menüler, orantılı genişlikler), filtrelenmiş düğümleri yalnızca gizlemek yerine DOM'dan gerçekten kaldırmanız gerekir. Veya JavaScript tarafından yönetilen dizinlere geri dönün.
Not: görünürlük: gizli ve opasite: 0 da sayılır, ancak bu öğeler hâlâ yer kapladığı için bu daha sezgisel geliyor. ekran: hiçbiri sinsi olan değil çünkü öğe görsel olarak kaybolur ancak yine de bir DOM yuvasını işgal eder.
Özel Özellikler Hemen Değerlendirin
Bu çok ince bir davranış. Dizini bir ebeveyn üzerinde merkezileştirmeye çalışırsanız:
.parent { --idx: kardeş-index(); }
…bu --idx tam orada .parent üzerinde çözümlenir. Ebeveynin kendi kardeş indeksini alır, bu sayıya kilitler ve her çocuk bu tek sabit değeri miras alır. Her çocuğa aynı numara verilir. Neredeyse kesinlikle istediğiniz veya beklediğiniz şey değil.
Çözümü basit; işlevi ona ihtiyaç duyan öğelerin üzerine yerleştirin:
.child { --idx: kardeş-index(); animasyon gecikmesi: calc(var(--idx) * 100ms); }
CSSWG, @property'ye bunu teorik olarak düzeltebilecek bir inherits: bildirimi eklemeyi tartıştı. @property kullanmadıysanız, özel bir özelliğin türünü, başlangıç değerini ve kalıtım davranışını tanımlamanıza olanak tanır; ham --variable'dan çok daha fazla kontrol. Ancak inherits: deklarasyon fikri henüz CSSWG'nin erken aşamasındadır ve herhangi bir spesifikasyon taslağına dahil edilmemiştir. İnmesi yıllar alabilir veya hiç inmeyebilir. Bugün @property'de bile 'henüz değerlendirme yapmayın, çocuğu bekleyin' diyen bir mekanizma yok. Şimdilik doğrudan başvurmanız yeterli.
Geniş Ölçekte Performans
DOM'yi değiştirmek (yani çocukları eklemek, çıkarmak, yeniden sıralamak) etkilenen kardeşler için stilin yeniden hesaplanmasını tetikler. Tarayıcı bunu basamaklama aşamasında (düzenleme ve boyamadan önce) gerçekleştirir, dolayısıyla JavaScript'te döngü yapma ve satır içi stilleri damgalama şeklindeki eski yaklaşımdan daha hızlıdır.
Ama eğer zorlarsan gerçek bir maliyeti olur. 10.000 alt öğe içeren bir kapsayıcının başına bir öğe eklemek, motoru kendisinden sonraki 10.000 öğenin tümü için kardeş dizinini yeniden hesaplamaya zorlar. Normal şeylerde (gezinme, kart ızgaraları, sekme çubukları) bunu asla fark etmeyeceksiniz. Canlı bir hisse senedi sayacı veya binlerce düğümün sürekli olarak değiştiği sonsuz kaydırmalı bir yayın için sanallaştırma pencerenizde JavaScript tarafından yönetilen dizinleri kullanmaya devam edin. Bu işlevler hızlıdır. Sıfır maliyetli değiller.
Bu yazının yazıldığı an itibarıyla Chrome/Edge 138, bu işlevleri kararlı sürümlerde (Haziran 2025) sundu ve bunu Safari 26.2 izledi. Firefox bunları henüz kararlı bir şekilde göndermedi, ancak Mozilla'nın teknik özellikleri olumlu ve uygulama çalışmaları aktif olarak sürüyor; Bugzilla'nın 1953973 numaralı sayısı altında takip ediliyor. Göndermeden önce caniuse'u en son gelişmeler için kontrol edin.
Chrome ve Safari birlikte küresel trafiğin yaklaşık %75-80'ini kapsıyor. Bu güçlü bir çoğunluk, ancak Firefox'un yokluğu hala bir yedeğe ihtiyacınız olduğu anlamına geliyor.
Bugün gönderim için @supports arkadaşınızdır:
/* Her yerde çalışan taban çizgisi */ .item { width: 25%; animasyon gecikmesi: 0 ms; } /* Desteklendiği yerde aşamalı olarak geliştir */ @supports (z-index: kardeş-index()) { .item { width: calc(100% / kardeş-count()); animasyon gecikmesi: calc(kardeş-index() * 80ms); } }
Firefox için statik geri dönüş. Herkes için matematiksel düzen. Kimse kırık bir sayfa alamaz.
Kardeşler arasında dolaşan ve satır içi stilleri ayarlayan bir JavaScript çoklu doldurma, bu işlevlerin yerini alacak olan şeydir. Ancak bu, sabit kodlanmış geri dönüş değerlerine takılıp kalacağınız anlamına da gelmez. Juan Diego RodrÃguez, yerel destek Baseline'a ulaşana kadar aşamalı iyileştirme için doğru modeli ortaya koyan "Kardeş sayımı() ve kardeş indeksi() İşlevleri Nasıl Beklenir?" hakkında sağlam bir makale yazdı. Yaklaşımı, tam bir JavaScript çoklu doldurma yerine bir köprü olarak mevcut CSS tekniklerini (Roman Komarov'un sayma hack'leri gibi) kullanıyor. Firefox yetişirken bugün üretime hazır bir şey göndermeniz gerekiyorsa okumaya değer.
Erişilebilirlik Notları
Bunu söylemek gerekiyor çünkü heyecanlanmak ve unutmak kolaydır: bu işlevler tamamen görseldir. Nesnelerin görünüşünü değiştirirler. Bir şeyin ne anlama geldiğini değiştirmezler.
Bir listeyi görsel olarak yeniden sıralamak için (sipariş veya ızgara yerleştirme yoluyla) sibling-index() matematiğini kullanırsanız, bir ekran okuyucu yine de DOM'u kaynak sırasına göre okur. Klavye sekme sırası da DOM'u takip eder. Görsel düzen ve anlamsal yapı birbiriyle çelişecektir ve bu bir erişilebilirlik hatasıdır.
Veri ızgaraları, radyal menüler veya düzen için ağaç sayımına dayanan özel liste kutuları gibi etkileşimli bileşenler için ARIA niteliklerini senkronize etmek üzere yine de JavaScript'e ihtiyacınız vardır. aria-posinset ve aria-setsize'ın CSS'nin ne hesapladığı hakkında hiçbir fikri yok. CSS'niz "bu, görsel olarak 7'nin 3'üncü öğesi" diyor ancak ARIA farklı bir şey söylüyorsa (veya hiçbir şey söylemiyorsa), yardımcı teknoloji kullanıcıları bozuk bir deneyim yaşar.
Hata ayıklama tarafında, Chrome DevTools'un son sürümleri, hesaplanan sibling-index() ve sibling-count() değerlerini doğrudan E'de incelemenize olanak tanır Matematik beklediğiniz şeyi yapmadığında yardımcı olan lements paneli.
Mevcut spesifikasyon yalnızca tüm öğe kardeşlerini sayar. Ancak CSSWG, #9572 sayısında planlı bir uzantıyı belgeledi: of
Sibling-index(of .active) gibi bir şey yalnızca belirli bir seçiciyle eşleşen kardeşleri saymanıza olanak tanır. Genel olarak sekizinci çocuk olan ancak üçüncü aktif çocuk olan bir öğe 3 değerini döndürecektir. Görünürlüğü filtrelediğiniz veya değiştirdiğiniz dinamik kullanıcı arayüzleri için bu, DOM manipülasyonu gerektirmeden dizini sıralı tutar.
Children-count() ve soyundan gelen-count() işlevleri hakkında da CSSWG tartışması vardı; ilki size bir öğenin kaç çocuğa sahip olduğunu söyler (ebeveyn odaklı düzenler için kullanışlıdır), ikincisi ise tüm alt öğeleri yinelemeli olarak sayar. Her ikisi de hala teklif aşamasında, ancak ağaç sayma hikayesini tamamlayacaklar: sibling-index() ve sibling-count() size yatay görünümü verirken (akranlarım arasında neredeyim?), Children-count() ve soyundan-count() size dikey görünümü verir (altımda ne var?).
Yukarıda bahsettiğim duygu — kademeli bir animasyon için on :nth-child() kuralı yazmak ve bariz bir şeyi kaçırıp kaçırmadığınızı merak etmek? Sen değildin. Açık olan şey henüz mevcut değildi.