React’te “Skeleton” Değil, “Shimmer” Değil: İçerik-Öncelikli (Content-Aware) Loading UI Tasarımı
Tek tip skeleton yerine, veri şekline göre yükleme arayüzleriyle algılanan performansı artırın.
Yükleme ekranı denince çoğu projede refleks aynı: her yerde gri kutular (skeleton). Ancak bu yaklaşım her zaman en iyi algıyı vermez. Kullanıcı aslında hangi içeriğin geleceğini anlamak ister. Bu yazıda “content-aware loading UI” yaklaşımıyla, yükleme durumunu verinin biçimine göre tasarlayıp daha anlaşılır ve daha az “zıplayan” (layout shift) arayüzler üretmeyi konuşacağız.
Problem: Tek tip skeleton neden yetersiz?
- Belirsizlik: Skeleton, gelecek içeriğin türünü anlatmıyorsa kullanıcı “ne beklediğini” bilmez.
- Yanıltıcı hız: Shimmer animasyonu bazen “hızlıymış” hissi verir ama gecikme uzadığında dikkat dağıtır.
- Layout shift riski: İçerik gelince boyutlar değişiyorsa sayfa gözle görülür şekilde oynar.
Hedef: Yükleme UI’ı, gelecek içeriğin yapısını dürüstçe temsil etsin ve yerleşim baştan otursun.
Yaklaşım: “İçerik Şekli” (content shape) ile yükleme
Bir liste sayfasını düşünelim: ürün kartı, fiyat, etiket, buton. Skeleton yerine, kartın semantiğini anlatan “placeholder”lar üretelim:
- Görsel alanı için sabit oran kutu
- Başlık için 2 satır
- Fiyat için kısa bir satır
- Etiket için küçük bir kapsül
1) Yerleşimi sabitle: aspect-ratio + min-height
function ProductCardShell() {
return (
<div className="card" aria-hidden="true">
<div className="media" />
<div className="lines">
<div className="line w-80" />
<div className="line w-60" />
<div className="row">
<div className="pill w-24" />
<div className="line w-20" />
</div>
</div>
</div>
);
}
.card { display: grid; gap: 12px; }
.media { aspect-ratio: 4 / 3; background: #eee; border-radius: 12px; }
.lines { display: grid; gap: 8px; }
.line { height: 12px; background: #eee; border-radius: 6px; }
.pill { height: 20px; background: #eee; border-radius: 999px; }
.w-80 { width: 80%; }
.w-60 { width: 60%; }
.w-24 { width: 96px; }
.w-20 { width: 72px; }
.row { display: flex; align-items: center; justify-content: space-between; }
Bu tasarımın farkı: “gri kutu” değil, ürün kartı geleceği belli.
2) Sayı kadar shell üret: boşluk hissini azalt
function ProductGrid({ products, isLoading }) {
if (isLoading) {
return (
<div className="grid">
{Array.from({ length: 8 }).map((_, i) => (
<ProductCardShell key={i} />
))}
</div>
);
}
return (
<div className="grid">
{products.map(p => (
<ProductCard key={p.id} product={p} />
))}
</div>
);
}
İpucu: Shell sayısını ekran genişliğine göre değil, kullanıcının ilk bakışta göreceği “above the fold” sayıya göre belirlemek genelde daha iyi hissettirir.
İçerik-öncelikli durumlar: “yok” ile “yükleniyor”yu ayır
Sık yapılan hata: veri yokken de skeleton göstermek. Oysa “0 sonuç” ayrı bir durumdur.
function Results({ data, isLoading, query }) {
if (isLoading) return <ResultsShell />;
if (!data.length) {
return (
<div role="status">
<h3>Sonuç bulunamadı</h3>
<p>“{query}” için farklı bir arama deneyin.</p>
</div>
);
}
return <ResultsList items={data} />;
}
Bu ayrım, kullanıcıya “sistem çalışıyor mu, yoksa gerçekten sonuç yok mu?” sorusunun cevabını net verir.
Erişilebilirlik: placeholder’ları sessiz tut
- Shell bileşenleri okuyucu teknolojilere içerik gibi görünmemeli.
- Yükleme başladığında küçük bir metinle durum bildirimi faydalı olabilir.
Öneri:
- Shell:
aria-hidden="true" - Sayfa genelinde:
role="status"içeren küçük bir “Yükleniyor…” metni
Animasyon kullanacaksan: “reduce motion”a saygı
Shimmer yerine hafif bir “pulse” tercih edin ve hareket azaltma ayarını kontrol edin:
@media (prefers-reduced-motion: reduce) {
.pulse { animation: none; }
}
Sonuç
İyi bir loading UI, sadece “bekleme”yi maskelemek değil; gelecek içeriği doğru anlatmak ve yerleşimi baştan stabil kurmaktır. Skeleton’ı körlemesine kopyalamak yerine, veri şekline göre “content-aware” placeholder’lar tasarlayın: kullanıcı daha az şaşırır, arayüz daha profesyonel görünür.