组件详解#47:模态对话框Modal Dialog,focus trap和滚动锁缺一不可 | Component Deep Dive #47: Modal Dialog — Focus Trap and Scroll Lock Are Non-Negotiable
组件详解#47:模态对话框Modal Dialog,focus trap和滚动锁缺一不可
弹窗谁都会写,但一个”对的”弹窗需要处理焦点陷阱、滚动锁定、键盘关闭、遮罩点击关闭。少一样都不专业。今天我们用原生<dialog>元素实现一个完整的Modal。
组件是什么
Modal Dialog(模态对话框)是一个覆盖在页面上的弹出层,用户必须先与对话框交互才能返回主页面。与Toast/Notification不同,Modal是”模态”的——它会阻断对底层页面的操作。
完整代码
HTML:
<dialog id="myModal" class="modal">
<div class="modal-box">
<div class="modal-header">
<h2>确认删除</h2>
<button class="modal-close" aria-label="关闭">×</button>
</div>
<div class="modal-body">
<p>此操作不可撤销,确定要删除吗?</p>
</div>
<div class="modal-footer">
<button class="btn-cancel">取消</button>
<button class="btn-confirm">确认删除</button>
</div>
</div>
</dialog>
<button onclick="document.getElementById('myModal').showModal()">打开弹窗</button>
CSS:
/* 遮罩层(原生dialog::backdrop) */
.modal::backdrop {
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(2px);
}
/* 对话框本体 */
.modal {
border: none;
border-radius: 12px;
padding: 0;
max-width: 480px;
width: 90%;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
animation: modalIn 0.25s ease;
}
@keyframes modalIn {
from { opacity: 0; transform: scale(0.95) translateY(10px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.modal-box { padding: 0; }
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18px 24px;
border-bottom: 1px solid #e2e8f0;
}
.modal-header h2 { margin: 0; font-size: 18px; }
.modal-close {
background: none; border: none; font-size: 24px;
cursor: pointer; color: #94a3b8; padding: 0; line-height: 1;
}
.modal-close:hover { color: #1e293b; }
.modal-body { padding: 20px 24px; color: #475569; line-height: 1.6; }
.modal-footer {
display: flex; justify-content: flex-end; gap: 10px;
padding: 16px 24px; border-top: 1px solid #e2e8f0;
}
.btn-cancel, .btn-confirm {
padding: 8px 20px; border-radius: 8px; font-size: 14px;
cursor: pointer; border: none; transition: all 0.2s;
}
.btn-cancel { background: #f1f5f9; color: #475569; }
.btn-cancel:hover { background: #e2e8f0; }
.btn-confirm { background: #ef4444; color: #fff; }
.btn-confirm:hover { background: #dc2626; }
JavaScript(焦点陷阱 + ESC关闭 + 遮罩关闭):
const modal = document.getElementById('myModal');
const closeBtn = modal.querySelector('.modal-close');
const cancelBtn = modal.querySelector('.btn-cancel');
// 关闭函数
function closeModal() { modal.close(); }
closeBtn.addEventListener('click', closeModal);
cancelBtn.addEventListener('click', closeModal);
// 点击遮罩区域关闭(不点击对话框本身)
modal.addEventListener('click', function(e) {
if (e.target === modal) closeModal();
});
// 焦点陷阱:Tab键不跑出对话框
modal.addEventListener('keydown', function(e) {
if (e.key !== 'Tab') return;
const focusable = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusable.length === 0) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault(); last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault(); first.focus();
}
});
关键技术点
- 原生
<dialog>元素:showModal()方法打开模态对话框,浏览器自动处理遮罩层(::backdrop)、焦点管理、ESC键关闭。比手动实现div遮罩方案可靠得多。 - 焦点陷阱(Focus Trap):Tab键不能跑出对话框。用
keydown监听Tab,在第一个和最后一个可聚焦元素之间循环。 - body滚动锁:原生
<dialog>在showModal()时会自动锁定底层页面的滚动,无需额外代码。如果用div方案,需要document.body.style.overflow = 'hidden'。
常见坑点
show()vsshowModal():show()打开的是非模态对话框(不锁底层),showModal()才是模态。别用错。- 动画闪烁:原生
<dialog>打开时默认无动画。用CSSanimation配合[open]属性选择器,或在JS中用requestAnimationFrame触发。 - ESC关闭行为覆盖:原生
<dialog>按ESC会自动关闭。如果需要阻止(如表单未保存),监听cancel事件并e.preventDefault()。
变体拓展
// 阻止ESC关闭(表单未保存时)
modal.addEventListener('cancel', function(e) {
if (hasUnsavedChanges) {
e.preventDefault();
showWarning('请先保存或放弃更改');
}
});
想要83个完整组件源码?访问 Web Component Dictionary 获取完整版。
Component Deep Dive #47: Modal Dialog — Focus Trap and Scroll Lock Are Non-Negotiable
Anyone can write a popup, but a “correct” modal needs focus trapping, scroll locking, keyboard close, and backdrop click close. Miss any one and it’s unprofessional. Today we’ll use the native <dialog> element.
Key Technical Points
- Native
<dialog>:showModal()opens a modal dialog — the browser handles backdrop (::backdrop), focus management, and ESC-to-close automatically. Far more reliable than manualdivoverlay. - Focus Trap: Tab key must not escape the dialog. Listen for Tab and cycle between first and last focusable elements.
- Body scroll lock: Native
<dialog>withshowModal()automatically locks underlying page scroll — no extra code needed. Withdivapproach, you needdocument.body.style.overflow = 'hidden'.
Common Pitfalls
show()vsshowModal():show()opens a non-modal dialog (no background lock);showModal()is modal. Don’t mix them up.- Animation flicker: Native
<dialog>has no open animation by default. Use CSSanimationwith[open]selector, or trigger withrequestAnimationFramein JS. - ESC close override: Native
<dialog>auto-closes on ESC. To prevent (e.g., unsaved form), listen tocancelevent ande.preventDefault().
Want all 83 complete component source codes? Visit Web Component Dictionary for the full version.