這篇文我們來記錄一下網頁中常常使用到的 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
函式。函式在一開始會先為元素指定 opacity
、display
和 visibility
,接著會運用 setInterval
頻繁地增加 opacity
直到其 opacity
為 1 時停止 timer
。
fadeOut
則是反向操作,將起始 opacity
定義為 1,頻繁減少 DOM 元素的 opacity
,直到其 opacity 為 0 時停止 timer
,並指定元素的 display
和 visiblity
。
blockScroll
和 unblockScroll
則會在 Modal 開啟和關閉時分別鎖住滾動軸,避免 Modal 在使用時畫面仍然可以被捲動。
運用 CSS 實現——運用 Label 及 Input 的特性配合兄弟選擇器
label
是一個非常有意思的 HTML 元素,它可以藉由 for
屬性和 input
的 id
屬性匹配在一起。也就是說,當我們有:
<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
去觸發對應 id
的 input
。也就是說,<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-flag
為 checked
狀態時讓 body
不被捲動,但 :has
相對較新,除了對瀏覽器版本的要求較高外,目前還沒有被所有主流瀏覽器所支援。
以上就是關於簡易 Modal 使用 JS 和純 CSS 的作法,如果這篇文有任何錯誤,也歡迎不吝指出。
References: