05Modal kính
Bấm nút xác nhận, panel modal chất liệu kính mở ra từ trạng thái thu nhỏ bằng opacity và scale, màn hình phía sau vẫn nhìn thấy nhưng đã mờ đi.
Dùng choModal xác nhận xóa, hộp thoại xác nhận thanh toán
opacityscale600ms

<main class="stage">
<div class="gm-scene is-demo">
<input type="checkbox" id="gm-open" class="gm-check">
<label class="gm-trigger" for="gm-open">Delete item</label>
<div class="gm-overlay"></div>
<div class="gm-modal gm-focal">
<span class="gm-modal__title">Delete this file?</span>
<span class="gm-modal__desc">This can't be undone.</span>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-enter;
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-ink);
// 트리거는 위쪽에, 모달은 장면 전체를 덮는 오버레이 안에서 따로 중앙 정렬한다 —
// 처음엔 둘 다 grid place-items:center 로 같은 자리에 겹쳐 렌더됐다 (실측: poster.png 에서 글자가 포개짐)
.gm-scene {
position: relative;
box-sizing: border-box;
width: min(340px, 90vw);
aspect-ratio: 340 / 190;
display: flex;
justify-content: center;
padding-top: $sp-4;
}
.gm-check { position: absolute; opacity: 0; pointer-events: none; }
.gm-trigger {
position: relative;
z-index: 0;
align-self: flex-start;
box-sizing: border-box;
padding: $sp-2 $sp-4;
border-radius: $r-control;
background: rgba(255, 255, 255, .12);
border: 1px solid rgba(255, 255, 255, .3);
color: $subject-cream;
font: 600 12px/1 -apple-system, "Segoe UI", sans-serif;
cursor: pointer;
}
.gm-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, .35);
opacity: 0;
transition: opacity $duration $easing;
pointer-events: none;
}
.gm-modal {
position: absolute;
inset: 0;
margin: auto;
z-index: 2;
box-sizing: border-box;
width: min(210px, 74%);
height: fit-content;
padding: $sp-5 $sp-4;
border-radius: $r-card;
background: rgba(255, 255, 255, .18);
border: 1px solid rgba(255, 255, 255, .38);
backdrop-filter: blur(18px) saturate(160%);
-webkit-backdrop-filter: blur(18px) saturate(160%);
box-shadow: $shadow-raised;
display: flex;
flex-direction: column;
gap: $sp-2;
color: $subject-cream;
font: 600 12px/1.4 -apple-system, "Segoe UI", sans-serif;
opacity: 0;
transform: scale(.85);
transition: opacity $duration $easing, transform $duration $easing;
pointer-events: none;
}
.gm-modal__title { font-size: 14px; font-weight: 700; }
.gm-modal__desc { opacity: .8; font-weight: 500; }
// 실사용: 체크박스 상태만으로 오버레이 + 모달이 열린다
.gm-check:checked ~ .gm-overlay { opacity: 1; }
.gm-check:checked ~ .gm-modal { opacity: 1; transform: scale(1); }
// 데모 루프: is-demo 는 열림·닫힘을 keyframes 로 반복 (체크박스 상태와 무관한 별도 경로)
@keyframes gm-overlay-loop {
0%, 12% { opacity: 0; }
35%, 78% { opacity: 1; }
100% { opacity: 0; }
}
@keyframes gm-modal-loop {
0%, 12% { opacity: 0; transform: scale(.85); }
35%, 78% { opacity: 1; transform: scale(1); }
100% { opacity: 0; transform: scale(.85); }
}
.gm-scene.is-demo .gm-overlay { animation: gm-overlay-loop $dur-loop $easing infinite; }
.gm-scene.is-demo .gm-modal { animation: gm-modal-loop $dur-loop $easing infinite; }
@media (prefers-reduced-motion: reduce) {
.gm-overlay, .gm-modal { transition: none; }
.gm-scene.is-demo .gm-overlay,
.gm-scene.is-demo .gm-modal { animation: none !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.gm-scene {
position: relative;
box-sizing: border-box;
width: min(340px, 90vw);
aspect-ratio: 340/190;
display: flex;
justify-content: center;
padding-top: 16px;
}
.gm-check {
position: absolute;
opacity: 0;
pointer-events: none;
}
.gm-trigger {
position: relative;
z-index: 0;
align-self: flex-start;
box-sizing: border-box;
padding: 8px 16px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.3);
color: #fff7e6;
font: 600 12px/1 -apple-system, "Segoe UI", sans-serif;
cursor: pointer;
}
.gm-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.35);
opacity: 0;
transition: opacity 600ms cubic-bezier(0.2, 0.8, 0.2, 1);
pointer-events: none;
}
.gm-modal {
position: absolute;
inset: 0;
margin: auto;
z-index: 2;
box-sizing: border-box;
width: min(210px, 74%);
height: fit-content;
padding: 20px 16px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.18);
border: 1px solid rgba(255, 255, 255, 0.38);
backdrop-filter: blur(18px) saturate(160%);
-webkit-backdrop-filter: blur(18px) saturate(160%);
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
display: flex;
flex-direction: column;
gap: 8px;
color: #fff7e6;
font: 600 12px/1.4 -apple-system, "Segoe UI", sans-serif;
opacity: 0;
transform: scale(0.85);
transition: opacity 600ms cubic-bezier(0.2, 0.8, 0.2, 1), transform 600ms cubic-bezier(0.2, 0.8, 0.2, 1);
pointer-events: none;
}
.gm-modal__title {
font-size: 14px;
font-weight: 700;
}
.gm-modal__desc {
opacity: 0.8;
font-weight: 500;
}
.gm-check:checked ~ .gm-overlay {
opacity: 1;
}
.gm-check:checked ~ .gm-modal {
opacity: 1;
transform: scale(1);
}
@keyframes gm-overlay-loop {
0%, 12% {
opacity: 0;
}
35%, 78% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes gm-modal-loop {
0%, 12% {
opacity: 0;
transform: scale(0.85);
}
35%, 78% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(0.85);
}
}
.gm-scene.is-demo .gm-overlay {
animation: gm-overlay-loop 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.gm-scene.is-demo .gm-modal {
animation: gm-modal-loop 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@media (prefers-reduced-motion: reduce) {
.gm-overlay, .gm-modal {
transition: none;
}
.gm-scene.is-demo .gm-overlay,
.gm-scene.is-demo .gm-modal {
animation: none !important;
}
}// 체크박스가 진짜 열림·닫힘을 낸다. 실제로 누르면 데모 루프(.is-demo)만 뗀다.
document.querySelector('.gm-check').addEventListener('change', function () {
document.querySelector('.gm-scene').classList.remove('is-demo');
});import { CSSProperties, useState } from "react";
import styles from "./GlassModal.module.scss";
type Vars = CSSProperties & {
"--duration"?: string;
"--easing"?: string;
};
export interface GlassModalProps {
duration?: string;
easing?: string;
}
export default function GlassModal({
duration = "600ms",
easing = "cubic-bezier(.2, .8, .2, 1)",
}: GlassModalProps) {
const [open, setOpen] = useState(false);
const style: Vars = { "--duration": duration, "--easing": easing };
return (
<div className={styles.scene} style={style}>
<button className={styles.trigger} type="button" onClick={() => setOpen(true)}>
Delete item
</button>
<div
className={`${styles.overlay} ${open ? styles.isOpen : ""}`}
onClick={() => setOpen(false)}
/>
<div className={`${styles.modal} ${open ? styles.isOpen : ""}`}>
<span className={styles.title}>Delete this file?</span>
<span className={styles.desc}>This can't be undone.</span>
</div>
</div>
);
}05 Modal kính — Diện tích đo được 14.35% · khung hình chuyển động 10/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · 9 css glassmorphism — dán một file là chạy
Bài viết chứa demo này
9 css glassmorphism — dán một file là chạyGlassmorphism là đặt một tấm kính mờ lên màn hình: nền phía sau hiện lên mờ mềm còn chữ phía trên vẫn rõ nét. Chín hiệu Xem tất cả demo trong bài này


