Bir sticky header, sayfa kaydırıldığında ekranın üstünde kalan başlık çubuğudur. En sevdiğim varyasyonu ise "akıllı" olanıdır: aşağı kaydırırken yoldan çekilip gizlenir, kullanıcı yukarı çıkmaya başladığında ise hemen geri görünür. Bu davranış hem dikey alanı okumaya bırakır hem de gezinmeyi her an bir parmak hareketi uzakta tutar. Bu yazıda önce saf CSS ile sabit çubuğu, sonra da kaydırma yönünü algılayan gizlen/görün davranışını adım adım kuruyorum.
En basit yol: position: sticky
Çoğu durumda JavaScript'e bile ihtiyacın yok. position: sticky öğeyi normal akışta tutar ama belirttiğin eşiğe ulaşınca yapıştırır. Bir top değeri vermen şarttır; vermezsen hiç yapışmaz.
.site-header {
position: sticky;
top: 0;
z-index: 100;
background: #fff;
}
Bu kadar. Çubuk sayfanın akışında başlar, üst kenara değdiğinde sabitlenir ve geri kalan içerik altından kayar. fixed yerine sticky tercih etmenin avantajı, öğenin başlangıçta normal yer kaplamasıdır: altındaki içeriğin üstüne binmez, ekstra padding-top hesabı yapmana gerek kalmaz.
sticky çalışmıyorsa neredeyse her zaman iki sebepten biridir: ya bir top eşiği vermemişsindir, ya da bir üst öğede overflow: hidden / auto / scroll tanımlıdır. İkincisi sticky'yi sessizce devre dışı bırakır, çünkü öğe artık o kapsayıcının kaydırma bağlamına bağlanır.
Kaydırma yönüne göre gizle/göster
Asıl istediğimiz "akıllı" davranış için kaydırma yönünü bilmemiz gerekir. Mantık basit: her kaydırmada güncel scrollY değerini bir öncekiyle karşılaştır. Değer büyüdüyse kullanıcı aşağı, küçüldüyse yukarı gidiyordur. Aşağı giderken çubuğu yukarı kaydırıp gizler, yukarı giderken geri getiririz.
.site-header {
position: sticky;
top: 0;
transition: transform 0.25s ease;
}
.site-header.gizli {
transform: translateY(-100%);
}
transform: translateY(-100%) kullanmamın sebebi performanstır: transform ve opacity değişiklikleri tarayıcının kompozit katmanında işlenir, yani layout veya paint tetiklemeden, akıcı 60 fps animasyon verir. top veya height değiştirerek aynı etkiyi kovalamak çok daha pahalıdır.
Scroll dinleyicisini doğru yazmak
scroll olayı saniyede onlarca kez tetiklenir. Her tetiklemede DOM'u okuyup yazarsan sayfa takılır. Çözüm, işi requestAnimationFrame ile bir sonraki çizim karesine ertelemek ve aynı kare içinde birden fazla kez çalışmasını bir bayrakla engellemektir.
const header = document.querySelector('.site-header');
let sonScroll = 0;
let bekliyor = false;
function guncelle() {
const y = window.scrollY;
// En tepedeyken her zaman göster
if (y <= 0) {
header.classList.remove('gizli');
} else if (y > sonScroll && y > 80) {
// Aşağı iniyoruz ve eşiği geçtik -> gizle
header.classList.add('gizli');
} else if (y < sonScroll) {
// Yukarı çıkıyoruz -> göster
header.classList.remove('gizli');
}
sonScroll = y;
bekliyor = false;
}
window.addEventListener('scroll', () => {
if (!bekliyor) {
window.requestAnimationFrame(guncelle);
bekliyor = true;
}
}, { passive: true });
Birkaç ince ayrıntı bu kodu kullanışlı kılar:
{ passive: true }: tarayıcıya "bu dinleyicipreventDefaultçağırmayacak" der; böylece kaydırmayı bloklamadan, daha akıcı işler.y > 80eşiği: sayfanın en üstündeki ilk birkaç pikselde çubuğun titreyip durmasını önler. Kullanıcı biraz aşağı inmeden gizleme başlamaz.- En tepede her zaman göster:
y <= 0kontrolü, sayfa başına dönünce başlığın garantili görünmesini sağlar.
Yapışınca görünümü değiştirmek
Sık istenen bir başka detay: başlık sayfa tepesindeyken şeffaf/büyük, kaydırılınca arka planlı/küçük olsun. Bunu "yapıştı mı?" bilgisiyle bir sınıf ekleyerek yaparız. Saf CSS ile algılamanın temiz bir yolu IntersectionObserver'dır: başlığın hemen üstüne 1px'lik bir gözcü öğe koyar, o ekrandan çıkınca başlık yapışmış demektir.
const gozcu = document.querySelector('#header-gozcu');
const io = new IntersectionObserver(([giris]) => {
header.classList.toggle('yapisik', !giris.isIntersecting);
}, { threshold: 0 });
io.observe(gozcu);
.site-header.yapisik {
box-shadow: 0 2px 12px rgba(0,0,0,.08);
backdrop-filter: blur(8px);
}
IntersectionObserver'ın güzelliği, bunu scroll olayında getBoundingClientRect() çağırmadan, tarayıcının kendi planlamasıyla yapmasıdır — yani çok daha verimlidir.
Erişilebilirlik ve mobil
Sticky header'lar dikkatsiz kurulunca erişilebilirlik sorunlarına yol açar. Şunları gözden kaçırma:
- Çapa bağlantıları: sayfa içi
#bolumlinklerine tıklanınca hedef, sabit başlığın altında kalabilir.scroll-margin-top: 80px;hedef bölümlere verilince bu sorun çözülür. - Hareket tercihi:
@media (prefers-reduced-motion: reduce)içindetransition'ı kapat; hareketten rahatsız olan kullanıcılar için çubuk anında değişsin. - Klavye odağı: çubuk gizliyken içindeki bağlantılar
Tabile odaklanırsa görünür hâle gelmeli. Gizlemeyidisplay: noneile değil,transformile yaptığımız için bunu zaten karşılarız. - Mobilde yükseklik: küçük ekranlarda başlık fazla dikey alan yemesin; gizlen/göster davranışı burada özellikle değerlidir çünkü okurken tüm ekranı geri verir.
@media (prefers-reduced-motion: reduce) {
.site-header { transition: none; }
}
section[id] { scroll-margin-top: 80px; }
Sık Sorulan Sorular
position: sticky ile fixed arasındaki fark ne?
fixed öğeyi normal akıştan tamamen çıkarır ve her zaman viewport'a sabitler; altındaki içeriğin üstüne biner, bu yüzden manuel padding-top gerekir. sticky ise öğe akışta başlar, yalnızca eşiğe ulaşınca yapışır ve kapsayıcısı bitince serbest bırakılır. Tam sayfa başlığı için çoğunlukla sticky daha temizdir.
Başlık kaydırırken titriyor/sıçrıyor, neden?
Genelde iki sebep: ya scroll olayını requestAnimationFrame ile sınırlamamışsındır, ya da en tepedeki ilk piksellerde gizle/göster eşiği yoktur. y > 80 gibi bir eşik ve rAF ile kareleme bu titremeyi giderir.
Sadece CSS ile gizlen/göster yapılabilir mi?
Tam olarak değil. position: sticky sabitlemeyi CSS ile çözer, ama "aşağı inince gizle, yukarı çıkınca göster" davranışı kaydırma yönünü bilmeyi gerektirir ve bunun için bugün hâlâ küçük bir JavaScript parçası en güvenilir yoldur.
Başlık çubuğun olması gerektiği gibi davranmıyor mu? Sticky header, açılır menü ve katman (z-index) sorunlarını birlikte akıcı ve erişilebilir hâle getirebiliriz. Bir projen varsa benimle iletişime geç.