彈跳視窗 (Modal)
📝 TL;DR
- Modal = 疊在頁面上的焦點對話框,務必處理「開啟/關閉、焦點鎖定、背景遮罩、Esc/點擊背景關閉」。
- 別用
display: none藏焦點:加上aria-modal="true"、role="dialog",並在開啟時把焦點移到對話框。 - 進階:多層 Modal、可捲內容、阻止背景捲動、動態插入內容。
什麼時候使用 Modal?
- 確認類行為:刪除、離開前提示
- 集中注意:表單、登入、快速檢視商品
- 非阻塞資訊不該用 Modal:提示可用 Toast/Inline
最小可用版本 (原生 JS)
html
<button id="open">開啟視窗</button>
<div id="modal" class="backdrop" hidden>
<div class="dialog" role="dialog" aria-modal="true" aria-labelledby="title">
<h2 id="title">確認刪除?</h2>
<p>刪除後無法復原。</p>
<button id="confirm">確定</button>
<button id="close">取消</button>
</div>
</div>
<style>
.backdrop {
position: fixed; inset: 0; display: grid; place-items: center;
background: rgba(0,0,0,0.45);
}
.dialog {
min-width: 320px; padding: 24px; border-radius: 12px;
background: white; box-shadow: 0 12px 32px rgba(0,0,0,0.2);
}
</style>
<script>
const modal = document.getElementById('modal')
const openBtn = document.getElementById('open')
const closeBtn = document.getElementById('close')
const confirmBtn = document.getElementById('confirm')
function openModal() {
modal.hidden = false
document.body.style.overflow = 'hidden' // 鎖定背景滾動
modal.querySelector('button').focus()
}
function closeModal() {
modal.hidden = true
document.body.style.overflow = ''
openBtn.focus()
}
openBtn.addEventListener('click', openModal)
closeBtn.addEventListener('click', closeModal)
confirmBtn.addEventListener('click', () => {
alert('已刪除')
closeModal()
})
modal.addEventListener('click', (e) => {
if (e.target === modal) closeModal() // 點擊背景關閉
})
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !modal.hidden) closeModal()
})
</script>模式設計清單
- 開啟/關閉:按鈕、Esc、背景點擊
- 焦點管理:開啟時聚焦首個可互動元素;關閉時還原到觸發源
- 滾動控制:開啟時
body禁止滾動,長內容可在對話框內滾 - 動畫:淡入/縮放即可,避免過度繁複
實戰練習
練習 1:背景點擊關閉(簡單)⭐
為現有 Modal 加上「點擊遮罩即關閉」。
💡 參考答案
javascript
modal.addEventListener('click', (e) => {
if (e.target === modal) closeModal()
})練習 2:阻止背景滾動(簡單)⭐
開啟 Modal 時禁止
body滾動,關閉後恢復。
💡 參考答案
javascript
function openModal() { modal.hidden = false; document.body.style.overflow = 'hidden' }
function closeModal() { modal.hidden = true; document.body.style.overflow = '' }練習 3:焦點圈(中等)⭐⭐
實作焦點鎖定:按 Tab 時焦點在對話框內循環,不會跑回背景。
💡 參考答案與提示
提示: 收集 dialog 內可聚焦元素,監聽 keydown Tab。
javascript
const focusables = modal.querySelectorAll('button, [href], input, select, textarea')
modal.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return
const first = focusables[0]
const last = focusables[focusables.length - 1]
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus() }
if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus() }
})延伸閱讀
- ARIA Authoring Practices: Dialog (Modal) Pattern
- Inclusive Components: The Toggle(含焦點管理技巧)
- MUI / Radix UI 的對話框元件實作,參考其 API 設計
FAQ
- Q: Modal 內可滾,背景也會滾?
- A: 開啟時設
body { overflow: hidden; },內容容器再設max-height+overflow: auto。
- A: 開啟時設
- Q: 多層 Modal 怎麼處理?
- A: 管理 z-index 階層,並只鎖定最上層的焦點與關閉行為。
- Q: SEO 會受影響嗎?
- A: Modal 內容通常非主要文案,重點是可及性;保留可關閉途徑即可。