Modal彈窗,可以說(shuō)是我們前端UI界面里的“標(biāo)配”了。但這個(gè)組件,恰恰是團(tuán)隊(duì)里代碼質(zhì)量的“重災(zāi)區(qū)”。
我見(jiàn)過(guò)太多用div
手寫(xiě)的彈窗了:z-index
滿(mǎn)天飛、焦點(diǎn)管理一塌糊涂、背景頁(yè)面還能滾動(dòng)、Esc
鍵也關(guān)不掉……這些問(wèn)題,每一個(gè)都是體驗(yàn)上的硬傷。
所以,最近我們團(tuán)隊(duì)的新項(xiàng)目,我立了一個(gè)規(guī)矩:只要是做模態(tài)對(duì)話(huà)框,一律優(yōu)先使用原生的<dialog>
元素。
它不僅能解決上面所有問(wèn)題,而且代碼量少得驚人。這篇文章,我就帶大家一步步地,用<dialog>
來(lái)構(gòu)建一個(gè)功能完善、可以直接拿到項(xiàng)目里用的Modal組件。
HTML骨架
我們不再需要一堆div
來(lái)模擬結(jié)構(gòu)。HTML的骨架非常簡(jiǎn)單清晰。
<button class="open-button">打開(kāi)彈窗</button>
<dialog class="my-modal">
<header class="modal-header">
<h2>我是彈窗標(biāo)題</h2>
<button class="close-button">×</button>
</header>
<div class="modal-body">
<p>這里是彈窗的主體內(nèi)容。</p>
<p>你可以試試按 Tab 鍵,焦點(diǎn)是不會(huì)跑到彈窗外面的。也可以按 Esc 鍵關(guān)閉。</p>
</div>
<footer class="modal-footer">
<button class="confirm-button">確認(rèn)</button>
</footer>
</dialog>
這個(gè)結(jié)構(gòu)里,header
, body
, footer
只是為了樣式清晰,核心就是那個(gè)<dialog>
標(biāo)簽。
核心功能 - 用JS喚醒
我們需要一個(gè)簡(jiǎn)單的腳本來(lái)控制dialog
的開(kāi)關(guān)。我們可以把它封裝成一個(gè)簡(jiǎn)單的類(lèi),方便復(fù)用。
class Modal {
constructor(dialogEl) {
if (!dialogEl || dialogEl.tagName !== 'DIALOG') {
console.error('需要一個(gè) <dialog> 元素');
return;
}
this.dialog = dialogEl;
this.closeButton = this.dialog.querySelector('.close-button');
this.handleBackdropClick = this.handleBackdropClick.bind(this);
this.init();
}
init() {
this.closeButton?.addEventListener('click', () => this.close());
this.dialog.addEventListener('click', this.handleBackdropClick);
}
open() {
this.dialog.showModal();
}
close() {
this.dialog.close();
}
handleBackdropClick(event) {
const rect = this.dialog.getBoundingClientRect();
const isInDialog = (
rect.top <= event.clientY && event.clientY <= rect.top + rect.height &&
rect.left <= event.clientX && event.clientX <= rect.left + rect.width
);
if (!isInDialog) {
this.close();
}
}
}
const dialogEl = document.querySelector('.my-modal');
const openBtn = document.querySelector('.open-button');
const modal = new Modal(dialogEl);
openBtn.addEventListener('click', () => modal.open());
代碼解析:
dialog.showModal()
: 這是關(guān)鍵。調(diào)用它,瀏覽器會(huì)自動(dòng)處理:
- 把
dialog
放到頁(yè)面的最頂層(top layer),z-index
再高也蓋不住它。 - 顯示一個(gè)默認(rèn)的遮罩層。
- 自動(dòng)管理焦點(diǎn),并將頁(yè)面背景“惰性化”。
dialog.close()
: 關(guān)閉彈窗。
點(diǎn)擊遮罩層關(guān)閉:這是原生<dialog>
默認(rèn)不帶的功能,但實(shí)現(xiàn)起來(lái)很簡(jiǎn)單。我們監(jiān)聽(tīng)dialog
本身的點(diǎn)擊事件,判斷點(diǎn)擊坐標(biāo)是否在dialog
的矩形區(qū)域內(nèi),如果不在,就說(shuō)明點(diǎn)的是遮罩層,此時(shí)調(diào)用close()
方法即可。
美化外觀(guān)
現(xiàn)在彈窗能工作了,但樣子還很丑。我們需要給它和它的遮罩層加點(diǎn)樣式。
.my-modal {
width: min(90vw, 500px);
border: none;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
padding: 0;
}
.modal-header, .modal-body, .modal-footer {
padding: 1rem 1.5rem;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #eee;
}
.close-button {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
}
.my-modal::backdrop {
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(3px);
}
::backdrop
這個(gè)偽元素所有關(guān)于遮罩層的樣式,都應(yīng)該寫(xiě)在這里。
增加動(dòng)畫(huà) - 讓體驗(yàn)更絲滑一些
默認(rèn)的dialog
是瞬間出現(xiàn)和消失的,體驗(yàn)有點(diǎn)生硬。我們可以加點(diǎn)動(dòng)畫(huà)。
.my-modal {
transition: opacity 0.3s, transform 0.3s;
}
.my-modal:not([open]) {
opacity: 0;
transform: translateY(30px);
}
.my-modal::backdrop {
transition: backdrop-filter 0.3s, background-color 0.3s;
}
.my-modal:not([open])::backdrop {
backdrop-filter: blur(0);
background-color: rgba(0, 0, 0, 0);
}
不過(guò),你會(huì)發(fā)現(xiàn)關(guān)閉動(dòng)畫(huà)不會(huì)生效,因?yàn)?code style="font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 0.87em; word-break: break-word; border-radius: 2px; overflow-x: auto; background-color: rgb(255, 245, 245); color: rgb(255, 80, 44); padding: 0.065em 0.4em;">dialog.close()會(huì)立刻讓元素從DOM中消失。要實(shí)現(xiàn)完美的關(guān)閉動(dòng)畫(huà),需要一個(gè)小技巧:
close() {
this.dialog.classList.add('is-closing');
this.dialog.addEventListener('animationend', () => {
this.dialog.classList.remove('is-closing');
this.dialog.close();
}, { once: true });
}
@keyframes slide-out {
from { opacity: 1; transform: translateY(0); }
to { opacity: 0; transform: translateY(30px); }
}
.my-modal.is-closing {
animation: slide-out 0.3s ease-out forwards;
}
這個(gè)方法稍微復(fù)雜一點(diǎn),但能實(shí)現(xiàn)完美的關(guān)閉動(dòng)畫(huà)。對(duì)于大部分簡(jiǎn)單場(chǎng)景,沒(méi)有關(guān)閉動(dòng)畫(huà)也是可以接受的。
最后我們看看兼容性:

到目前位置,主流瀏覽器幾乎都支持原生dialog,但是值得注意的是 Safari, 它的支持度比起它瀏覽器稍晚,2022年后才開(kāi)始支持,對(duì)于 Safari 瀏覽器 我更推薦大家使用等價(jià)的 polyfill 去解決。(推薦使用 Chrome 官方的 dialog-polyfill)
?
分享完畢,謝謝大家??
轉(zhuǎn)自https://juejin.cn/post/7532302427423817780
該文章在 2025/8/5 9:55:44 編輯過(guò)