運用 JS 或純 CSS 製作簡易 Modal

Eason Lin
14 min readDec 18, 2022

--

Photo by Boris Smokrovic on Unsplash

這篇文我們來記錄一下網頁中常常使用到的 Modal 在不依賴 Libarary 的情況下可以怎麼實現,並分別會實現有 JS 及純 CSS 的版本。

運用 JS 實現 — —使用 setInterval

我自己覺得相對 CSS,JS 的實現方式相對很好理解。淡入和淡出的效果我們可以使用 setInterval 在指定毫秒數內頻繁更新元素的透明度,製造出一種漸變的感覺:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.my-modal-container {
display: none;
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.3);
align-items: center;
justify-content: center;
}
.my-modal {
background-color: #fff;
display: inline-block;
border-radius: 10px;
padding: 12px;
max-width: 360px;
}
.btn {
border-radius: 6px;
padding: 4px 12px;
background-color: silver;
}
</style>
</head>
<body>
<button data-open-modal class="btn">Click me</button>
<h1>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Suscipit eligendi fugiat corrupti? Quam impedit nesciunt vitae ipsa doloremque aspernatur dolore, iure quisquam eveniet sapiente incidunt quibusdam repudiandae velit corporis. Aut?</h1>
<h1>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Numquam maiores, architecto eveniet doloremque, dignissimos velit non veniam sequi eligendi molestias, vel quas debitis laboriosam mollitia quos animi omnis quaerat suscipit?</h1>
<div class="my-modal-container">
<div class="my-modal">
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Harum voluptatum placeat iure quis non facilis veritatis earum reprehenderit accusamus aperiam.</p>
<button data-close-modal class="btn">Confirm</button>
</div>
</div>
<button data-open-modal class="btn">Click me</button>
<script>
const modal = document.querySelector('.my-modal-container');
const openBtn = document.querySelectorAll('*[data-open-modal]');
openBtn.forEach(node => {
node.addEventListener('click', () => {
fadeIn(modal, 'flex');
blockScroll();
});
});
const closeBtn = document.querySelectorAll('*[data-close-modal]');
closeBtn.forEach(node => {
node.addEventListener('click', () => {
fadeOut(modal);
unblockScroll();
});
});

function fadeIn (el, display="inline-block", duration=400) {
el.style.opacity = el.style.opacity || 0;
el.style.display = display;
el.style.visibility = "visible";

let opacity = parseFloat(el.style.opacity) || 0;
const timer = setInterval( function() {
opacity += 20 / duration;
if( opacity >= 1 ) {
clearInterval(timer);
opacity = 1;
}
el.style.opacity = opacity;
}, 20 );
};

function fadeOut(el, duration=400) {
let opacity = 1;
const timer = setInterval( function() {
opacity -= 20 / duration;
if(opacity <= 0) {
clearInterval(timer);
opacity = 0;
el.style.display = "none";
el.style.visibility = "hidden";
}
el.style.opacity = opacity;
}, 20);
};

function blockScroll() {
document.body.style.overflow = 'hidden';
}

function unblockScroll() {
document.body.style.overflow = null;
}
</script>
</body>
</html>

我們會將 Modal 的 DOM 元素傳入 fadeIn 函式。函式在一開始會先為元素指定 opacitydisplayvisibility,接著會運用 setInterval 頻繁地增加 opacity 直到其 opacity 為 1 時停止 timer

fadeOut 則是反向操作,將起始 opacity 定義為 1,頻繁減少 DOM 元素的 opacity,直到其 opacity 為 0 時停止 timer,並指定元素的 displayvisiblity

blockScrollunblockScroll 則會在 Modal 開啟和關閉時分別鎖住滾動軸,避免 Modal 在使用時畫面仍然可以被捲動。

實際操作(為了節省容量用手機寬度錄影)

運用 CSS 實現——運用 Label 及 Input 的特性配合兄弟選擇器

label 是一個非常有意思的 HTML 元素,它可以藉由 for 屬性和 inputid 屬性匹配在一起。也就是說,當我們有:

<label for="my-input">Click me!</label>
<input id="my-input" type="checkbox" />

它們就會是匹配的狀態,不論兩者生處在什麼位置,點擊 label 時都會觸發 input,且同一個 input 可以和多個 label 匹配。

兄弟選擇器(sibling combinator)可以用來選擇同層的 DOM 元素。在這裡因為範例的 Modal DOM 元素會建立在按鈕旁邊,所以我們採用相鄰兄弟選擇器。

相鄰兄弟選擇器可以用來選擇該元素的下一個元素,我們可以直接看範例:

.a + .b {
color: red;
}
<p class="b">1</p>
<p class="a">2</p>
<p class="b">3</p>
<p class="b">4</p>

在這個範例下,我有四個 class 分別為 b, a, b, b 的 p 元素,內容為 3 的元素因為相鄰且為 a 的下一個元素,因此被選擇到並採用了 color: red 的樣式;內容為 4 的元素與 2 並不相鄰,因此不採用;內容為 1 的元素雖然與 2 相鄰,但它是 2 的前一個元素因此也不採用。

知道以上特性後,我們就可以來實現簡易的純 CSS Modal:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.my-modal-container {
opacity: 0;
pointer-events: none;
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.3);
transition: opacity 0.4s linear;
display: flex;
align-items: center;
justify-content: center;
}
.my-modal {
background-color: #fff;
display: inline-block;
border-radius: 10px;
padding: 12px;
max-width: 360px;
}
.btn {
border-radius: 6px;
padding: 4px 12px;
background-color: silver;
}
#modal-flag {
display: none;
}
#modal-flag:checked + .my-modal-container {
opacity: 1;
pointer-events: unset;
}
</style>
</head>
<body>
<label class="btn" for="modal-flag">Click me</label>
<h1>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Suscipit eligendi fugiat corrupti? Quam impedit nesciunt vitae ipsa doloremque aspernatur dolore, iure quisquam eveniet sapiente incidunt quibusdam repudiandae velit corporis. Aut?</h1>
<h1>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Numquam maiores, architecto eveniet doloremque, dignissimos velit non veniam sequi eligendi molestias, vel quas debitis laboriosam mollitia quos animi omnis quaerat suscipit?</h1>
<input id="modal-flag" type="checkbox" />
<div class="my-modal-container">
<div class="my-modal">
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Harum voluptatum placeat iure quis non facilis veritatis earum reprehenderit accusamus aperiam.</p>
<label class="btn" for="modal-flag">Confirm</label>
</div>
</div>
</body>
</html>

在這裡,我們使用了兩個 label,兩者皆用了 for 去觸發對應 idinput。也就是說,<label class=”btn” for=”modal-flag”>Click me</label><label class=”btn” for=”modal-flag”>Confirm</label> 都會觸發 <input id=”modal-flag” type=”checkbox” /> 的開啟或關閉。

在 CSS 中,我們讓 <div class=”my-modal-container”>,也就是 modal 的 opacity 在預設狀態下為 0。

input 的狀態為 checked 時,會透過 #modal-flag:checked + .my-modal-container 選擇到 modal,並將它的 opacity 設定為 1。藉由 transition 就能實現淡入淡出的效果:

實際操作(為了節省容量用手機寬度錄影)

這樣的做會有個缺點:當 Modal 為關閉狀態時,實際上它依然是蓋在最上方,只是藉由 pointer-events: none 讓它不會被點擊到而已。有一個做法是將其替換為 visibility,但便會讓淡出時的 opacity 的效果消失。此外,這樣的作法在 Modal 開啟時,頁面依然會是可以捲動的狀態。我們可以使用 :has

body:has(#modal-flag:checked) {
overflow: hidden;
}

去實現『子層有某種狀態時選擇父層』來讓 #modal-flagchecked 狀態時讓 body 不被捲動,但 :has 相對較新,除了對瀏覽器版本的要求較高外,目前還沒有被所有主流瀏覽器所支援

--

--