Popover API ve dialog ile kütüphanesiz overlay
Bir dropdown menü ya da modal açmak için kaç kere Headless UI, Radix veya Floating UI kurdunuz? Bu kütüphaneler gerçek sorunları çözüyor: z-index savaşları, focus trap, erişilebilirlik. Ama tarayıcılar artık bu sorunların büyük kısmını native olarak çözebiliyor. Popover API ve <dialog> elementi birlikte kullanıldığında, JS kütüphanesi olmadan erişilebilir overlay'ler yapmak mümkün.
Bu yazıda iki farklı native mekanizmayı, hangi durumda hangisinin doğru seçim olduğunu ve ikisinin birlikte nasıl çalıştığını anlatacağım.
Top layer nedir ve neden z-index'ten farklıdır
Tarayıcılarda her belgenin normal stacking context'inin üzerinde bir katman var: top layer. Bu katmana yerleşen bir element, sayfadaki hiçbir z-index değerinden etkilenmez. position: fixed ile bile çözülemeyen "modal'ın arkasındaki dropdown'ın önüne geçme" sorunu burada ortadan kalkıyor.
Top layer'a element yerleştirmenin iki yolu var:
<dialog>elementininshowModal()metodu- Popover API'nin
popoverattribute'u
İkisi de elementi top layer'a taşır, ama davranışları farklıdır.
Popover API: non-modal overlay'ler
Popover API, 2024 itibarıyla tüm major tarayıcılarda (Chrome 114+, Firefox 125+, Safari 17+) destekleniyor. Tek satır JS yazmadan çalışan deklaratif bir API.
Temel kullanım
<button popovertarget="menu">Menüyü aç</button>
<div id="menu" popover>
<ul>
<li><a href="/profil">Profil</a></li>
<li><a href="/ayarlar">Ayarlar</a></li>
<li><button>Çıkış yap</button></li>
</ul>
</div>Bu kadar. Butona tıklayınca #menu açılır, tekrar tıklayınca veya dışarı tıklayınca kapanır. JavaScript yok. popover attribute'u varsayılan olarak auto değerini alır, bu da "light dismiss" davranışı sağlar: Escape tuşu veya dışarıya tıklama ile kapanma.
popover="manual" ile kontrollü açma
Bazı durumlarda overlay'in dışarı tıklayınca kapanmasını istemezsiniz. Toast bildirimleri buna iyi bir örnek.
<div id="toast" popover="manual">
İşlem başarılı.
</div>const toast = document.getElementById('toast');
toast.showPopover();
setTimeout(() => {
toast.hidePopover();
}, 3000);manual modda light dismiss devre dışıdır. Açma ve kapama tamamen sizin kontrolünüzde.
popovertargetaction ile açma/kapama ayrımı
Varsayılan davranış toggle'dır, ama bunu değiştirebilirsiniz:
<button popovertarget="panel" popovertargetaction="show">Aç</button>
<button popovertarget="panel" popovertargetaction="hide">Kapat</button>
<div id="panel" popover>
Panel içeriği
</div>Popover'ı CSS ile konumlandırma
Popover açıldığında top layer'a geçer ve varsayılan olarak viewport'un ortasına yerleşir. Bunu CSS ile değiştirmek kolay:
[popover] {
margin: 0;
position: fixed;
top: auto;
right: 1rem;
bottom: 1rem;
left: auto;
}Daha gelişmiş konumlandırma için CSS anchor positioning ile popover'ı tetikleyen butona bağlayabilirsiniz. Bu ikili birlikte çalışınca Floating UI'a olan ihtiyaç büyük ölçüde düşüyor.
::backdrop farkı
Popover'lar top layer'a çıktığında bir ::backdrop pseudo-elementi oluşur, ama varsayılan olarak görünmezdir. Bunu stilleyebilirsiniz:
[popover]::backdrop {
background: rgba(0, 0, 0, 0.15);
}Ancak dikkat: popover'ın backdrop'u etkileşimi engellemez. Kullanıcı backdrop'a tıklarsa popover kapanır (auto modda), ama arkadaki sayfayla etkileşim hâlâ mümkündür. Modal davranış istiyorsanız <dialog> kullanmanız gerekir.
dialog elementi: modal overlay'ler
<dialog> elementi popover'dan daha eski (Chrome 37'den beri var) ve farklı bir amaca hizmet ediyor. İki modu var: non-modal (show()) ve modal (showModal()). Modal modda davranış popover'dan belirgin şekilde ayrılır.
Modal dialog açma
<button id="open-btn">Silme onayı</button>
<dialog id="confirm-dialog">
<h2>Emin misiniz?</h2>
<p>Bu işlem geri alınamaz.</p>
<form method="dialog">
<button value="cancel">Vazgeç</button>
<button value="confirm">Sil</button>
</form>
</dialog>const dialog = document.getElementById('confirm-dialog');
const openBtn = document.getElementById('open-btn');
openBtn.addEventListener('click', () => {
dialog.showModal();
});
dialog.addEventListener('close', () => {
console.log(dialog.returnValue); // "cancel" veya "confirm"
});showModal() çağrıldığında üç şey olur:
- Element top layer'a taşınır
::backdropotomatik olarak görünür hale gelir ve arkadaki sayfayla etkileşimi engeller (inert davranışı)- Focus, dialog içine hapsolur (focus trap)
Üçüncü madde çok değerli. Kütüphanelerle focus trap yapmak error-prone bir iş; tarayıcının native implementasyonu Tab ve Shift+Tab döngüsünü doğru yönetiyor.
form method="dialog" ile kapama
Dialog içindeki <form method="dialog"> özel bir davranışa sahip: form submit edildiğinde dialog kapanır ve tıklanan butonun value değeri dialog.returnValue olarak atanır. HTTP isteği gönderilmez. Bu pattern, onay diyalogları için son derece pratik.
Escape ile kapama davranışı
Modal dialog varsayılan olarak Escape tuşuyla kapanır. Bunu engellemek istiyorsanız (mesela kaydedilmemiş değişiklik uyarısı göstermek için):
dialog.addEventListener('cancel', (e) => {
if (hasUnsavedChanges) {
e.preventDefault();
}
});cancel eventi close'dan önce tetiklenir ve preventDefault() ile iptal edilebilir.
Focus yönetimi detayları
Erişilebilir overlay'lerin en kritik parçası focus yönetimi. Tarayıcılar burada ne yapıyor?
dialog'da focus
showModal() çağrıldığında tarayıcı, dialog içindeki ilk focusable elemente otomatik olarak odaklanır. Eğer dialog içinde autofocus attribute'u olan bir element varsa, o tercih edilir:
<dialog id="search-dialog">
<input type="search" autofocus placeholder="Ara..." />
<button>Kapat</button>
</dialog>Dialog kapandığında focus, dialog'u açan elemente geri döner. Bunu elle yapmanıza gerek yok.
Popover'da focus
Popover API'da focus davranışı farklı. Popover açıldığında focus otomatik olarak popover içine taşınmaz. Bu bilinçli bir tasarım kararı: bir tooltip veya bilgi kutusu açıldığında kullanıcının focus'unu kaybetmesi genellikle istenmez.
Ancak popover içinde interaktif elementler varsa (dropdown menü gibi), focus'u elle yönetmeniz gerekebilir:
const menu = document.getElementById('menu');
menu.addEventListener('toggle', (e) => {
if (e.newState === 'open') {
const firstItem = menu.querySelector('a, button');
firstItem?.focus();
}
});toggle eventi, popover her açılıp kapandığında tetiklenir. e.newState değeri "open" veya "closed" olabilir.
Popover mu, dialog mu?
Bu iki API'nin kullanım alanları farklı. Seçim kararı genellikle şu sorulara bağlı:
Arkadaki sayfayla etkileşim engellensin mi? Evet ise <dialog> ile showModal(). Hayır ise popover.
Focus trap gerekli mi? Modal dialog'da tarayıcı bunu otomatik yapar. Popover'da yapmaz.
Dışarı tıklayınca kapansın mı? Popover auto modda bunu bedavaya verir. Dialog'da bunu kendiniz yazmanız gerekir:
dialog.addEventListener('click', (e) => {
if (e.target === dialog) {
dialog.close();
}
});Bu trick, dialog'un padding alanına tıklamayı yakalar. Dialog içindeki content bir wrapper <div> içinde olmalı ki iç tıklamalar yanlışlıkla dialog'u kapatmasın.
Pratik eşleştirmeler
Tooltip, dropdown menü, tarih seçici, bildirim pop-up'ı: popover.
Onay diyalogu, form modal'ı, tam ekran galeri: <dialog> + showModal().
Toast bildirimi: popover="manual" (light dismiss istemiyorsanız).
Animasyon ekleme
Top layer elementlerinin animasyonu eskiden zordu çünkü display: none'dan geçiş yapıyorlardı. CSS @starting-style kuralı ve transition-behavior: allow-discrete ile bu artık mümkün.
[popover] {
opacity: 0;
transform: translateY(8px);
transition: opacity 0.2s, transform 0.2s, display 0.2s;
transition-behavior: allow-discrete;
}
[popover]:popover-open {
opacity: 1;
transform: translateY(0);
}
@starting-style {
[popover]:popover-open {
opacity: 0;
transform: translateY(8px);
}
}Aynı pattern <dialog> için de geçerli; :popover-open yerine dialog[open] seçicisini kullanırsınız. Çıkış animasyonu için @starting-style gerekli çünkü tarayıcının elementin "açılmadan önceki" durumunu bilmesi lazım.
Sayfa geçişleri ile birlikte kullanmak isterseniz React ViewTransition yazısındaki yaklaşımlar da işe yarar, ama burada View Transitions API'ye gerek yok; CSS transition yeterli.
Erişilebilirlik kontrol listesi
Native API'ler birçok ARIA gereksinimini otomatik karşılar, ama birkaç şeyi elle eklemeniz gerekir.
Dialog için aria-labelledby ile başlığa referans verin:
<dialog aria-labelledby="dialog-title">
<h2 id="dialog-title">Dosyayı sil</h2>
<!-- ... -->
</dialog>Popover dropdown menü için role ve aria-expanded ekleyin:
<button popovertarget="nav-menu" aria-expanded="false" aria-haspopup="true">
Menü
</button>
<nav id="nav-menu" popover role="menu">
<a role="menuitem" href="/profil">Profil</a>
<a role="menuitem" href="/ayarlar">Ayarlar</a>
</nav>const navMenu = document.getElementById('nav-menu');
const trigger = document.querySelector('[popovertarget="nav-menu"]');
navMenu.addEventListener('toggle', (e) => {
trigger.setAttribute('aria-expanded', e.newState === 'open');
});Tarayıcı popovertarget ilişkisini tanır ama aria-expanded'ı otomatik güncellemez. Bu iki satırlık JS kaçınılmaz.
Ne zaman hâlâ kütüphane gerekir
Native API'ler çoğu durumu karşılar, ama sınırları var. Nested popover'lar (bir popover içinden başka bir popover açma) popover auto modda çalışır: ilk popover'ın içindeki popovertarget ikinci popover'ı açtığında ilk popover açık kalır. Ama üçüncü, dördüncü seviye iç içe geçmelerde davranış karmaşıklaşıyor.
Bir diğer sınır: popover'ın tetikleyiciye göre konumlanması. CSS anchor positioning henüz Firefox'ta desteklenmiyor (Temmuz 2025 itibarıyla). Eğer cross-browser desteğe ihtiyacınız varsa, konumlandırma için Floating UI hâlâ gerekli olabilir. Ama overlay'in kendisi, açılma/kapanma mantığı ve top layer davranışı için native API yeterli.
Bir modal'ın arkasındaki sayfayı scroll'dan kilitlemek de native dialog'un otomatik yaptığı bir şey (showModal() arkayı inert yapar). Popover'da bu yok, kendiniz overflow: hidden eklemeniz gerekir.
Kütüphanesiz overlay'ler artık bir "hack" değil, tarayıcı standardı. Önce popover ve <dialog> ile ne kadar ilerleyebildiğinize bakın, kalan boşluklar için kütüphaneye uzanın. Çoğu projede o boşluk düşündüğünüzden küçük çıkacak.