04Làm mờ nền
Màn hình phía sau mờ dần để tập trung vào modal.
Dùng choThanh toán, xác nhận quan trọng
8px어둡게만12px

<main class="stage is-demo">
<div class="page">
<div class="page__line page__line--a"></div>
<div class="page__line page__line--b"></div>
<div class="page__line page__line--c"></div>
<button class="trigger" type="button">Xác nhận thanh toán</button>
</div>
<div class="overlay"></div>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="t4">
<h2 id="t4">Thanh toán 67.000₫?</h2>
<p>Hãy kiểm tra kỹ trong lúc màn hình phía sau đang mờ đi.</p>
<button class="modal__close" type="button">Thanh toán</button>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-enter; // 배경이 흐려지는 시간
$easing: $ease-out;
$color: $subject-blue; // 강조색(버튼)
@include stage($stage-paper);
.stage { position: relative; }
// 뒤 화면 흉내 — 단색 배경만으로는 blur 가 보이지 않아 가짜 콘텐츠를 둔다
.page {
position: absolute; inset: 0; display: grid; place-items: center; gap: clamp(8px, 3vmin, 14px);
}
.page__line {
height: clamp(8px, 3vmin, 12px); border-radius: $r-control; background: rgba($stage-ink, .12);
&--a { width: 55%; } &--b { width: 40%; } &--c { width: 62%; }
}
.trigger {
border: 0; border-radius: $r-pill; margin-top: clamp(8px, 3vmin, 14px);
padding: clamp(10px, 3.4vmin, 14px) clamp(20px, 7vmin, 30px);
font-weight: 600;
font-size: clamp(13px, 4vmin, 16px);
line-height: 1;
font-family: inherit;
color: $stage-paper; background: $color;
cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.overlay {
position: fixed; inset: 0;
background: rgba($stage-ink, .28);
backdrop-filter: blur(0px);
opacity: 0; pointer-events: none;
transition: opacity $duration $easing, backdrop-filter $duration $easing;
}
.modal {
position: fixed; left: 50%; top: 50%;
transform: translate(-50%, -50%) scale(.94);
opacity: 0; pointer-events: none;
width: min(78vw, 300px);
padding: clamp(12px, 4.5vmin, 22px);
border-radius: $r-card; background: $stage-paper; color: $stage-ink;
box-shadow: $shadow-raised;
transition: opacity $duration $easing, transform $duration $easing;
h2 { margin: 0 0 6px; font-size: clamp(13px, 4.4vmin, 18px); }
p { margin: 0 0 14px; font-size: clamp(10px, 3.2vmin, 13px); color: rgba($stage-ink, .65); }
}
.modal__close {
border: 0; border-radius: $r-control; background: $color; color: $stage-paper;
padding: clamp(7px, 2.6vmin, 9px) clamp(14px, 4.6vmin, 18px);
font-weight: 600;
font-size: clamp(11px, 3.4vmin, 13px);
line-height: 1;
font-family: inherit;
cursor: pointer;
}
.stage.is-open .overlay { opacity: 1; backdrop-filter: blur(8px); pointer-events: auto; }
.stage.is-open .modal { opacity: 1; transform: translate(-50%, -50%) scale(1); pointer-events: auto; }
.stage.is-demo .overlay { animation: overlay-loop $dur-loop ease-in-out infinite; }
.stage.is-demo .modal { animation: modal-loop $dur-loop $easing infinite; }
@keyframes overlay-loop {
0% { opacity: 0; backdrop-filter: blur(0px); }
15% { opacity: 1; backdrop-filter: blur(8px); }
70% { opacity: 1; backdrop-filter: blur(8px); }
85% { opacity: 0; backdrop-filter: blur(0px); }
100% { opacity: 0; backdrop-filter: blur(0px); }
}
@keyframes modal-loop {
0% { opacity: 0; transform: translate(-50%, -50%) scale(.94); }
15% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
70% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
85% { opacity: 0; transform: translate(-50%, -50%) scale(.94); }
100% { opacity: 0; transform: translate(-50%, -50%) scale(.94); }
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .overlay, .stage.is-demo .modal { animation: none; }
.overlay, .modal { transition-duration: 1ms; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.stage {
position: relative;
}
.page {
position: absolute;
inset: 0;
display: grid;
place-items: center;
gap: clamp(8px, 3vmin, 14px);
}
.page__line {
height: clamp(8px, 3vmin, 12px);
border-radius: 12px;
background: rgba(23, 20, 26, 0.12);
}
.page__line--a {
width: 55%;
}
.page__line--b {
width: 40%;
}
.page__line--c {
width: 62%;
}
.trigger {
border: 0;
border-radius: 999px;
margin-top: clamp(8px, 3vmin, 14px);
padding: clamp(10px, 3.4vmin, 14px) clamp(20px, 7vmin, 30px);
font-weight: 600;
font-size: clamp(13px, 4vmin, 16px);
line-height: 1;
font-family: inherit;
color: #ffffff;
background: #2f6df6;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.overlay {
position: fixed;
inset: 0;
background: rgba(23, 20, 26, 0.28);
backdrop-filter: blur(0px);
opacity: 0;
pointer-events: none;
transition: opacity 600ms ease-out, backdrop-filter 600ms ease-out;
}
.modal {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) scale(0.94);
opacity: 0;
pointer-events: none;
width: min(78vw, 300px);
padding: clamp(12px, 4.5vmin, 22px);
border-radius: 16px;
background: #ffffff;
color: #17141a;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
transition: opacity 600ms ease-out, transform 600ms ease-out;
}
.modal h2 {
margin: 0 0 6px;
font-size: clamp(13px, 4.4vmin, 18px);
}
.modal p {
margin: 0 0 14px;
font-size: clamp(10px, 3.2vmin, 13px);
color: rgba(23, 20, 26, 0.65);
}
.modal__close {
border: 0;
border-radius: 12px;
background: #2f6df6;
color: #ffffff;
padding: clamp(7px, 2.6vmin, 9px) clamp(14px, 4.6vmin, 18px);
font-weight: 600;
font-size: clamp(11px, 3.4vmin, 13px);
line-height: 1;
font-family: inherit;
cursor: pointer;
}
.stage.is-open .overlay {
opacity: 1;
backdrop-filter: blur(8px);
pointer-events: auto;
}
.stage.is-open .modal {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
pointer-events: auto;
}
.stage.is-demo .overlay {
animation: overlay-loop 2s ease-in-out infinite;
}
.stage.is-demo .modal {
animation: modal-loop 2s ease-out infinite;
}
@keyframes overlay-loop {
0% {
opacity: 0;
backdrop-filter: blur(0px);
}
15% {
opacity: 1;
backdrop-filter: blur(8px);
}
70% {
opacity: 1;
backdrop-filter: blur(8px);
}
85% {
opacity: 0;
backdrop-filter: blur(0px);
}
100% {
opacity: 0;
backdrop-filter: blur(0px);
}
}
@keyframes modal-loop {
0% {
opacity: 0;
transform: translate(-50%, -50%) scale(0.94);
}
15% {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
70% {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
85% {
opacity: 0;
transform: translate(-50%, -50%) scale(0.94);
}
100% {
opacity: 0;
transform: translate(-50%, -50%) scale(0.94);
}
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .overlay, .stage.is-demo .modal {
animation: none;
}
.overlay, .modal {
transition-duration: 1ms;
}
}(function () {
var stage = document.querySelector('.stage');
var trigger = document.querySelector('.trigger');
var overlay = document.querySelector('.overlay');
var close = document.querySelector('.modal__close');
function open() { stage.classList.remove('is-demo'); stage.classList.add('is-open'); }
function shut() { stage.classList.remove('is-open'); }
trigger.addEventListener('click', open);
overlay.addEventListener('click', shut);
close.addEventListener('click', shut);
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') shut(); });
})();import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./BackdropBlurModal.module.scss";
export type BackdropBlurModalProps = {
triggerLabel: string;
title: string;
description: string;
confirmLabel: string;
/** 배경이 흐려지는 시간 (기본 600ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function BackdropBlurModal({
triggerLabel,
title,
description,
confirmLabel,
duration = 600,
easing = "ease-out",
color = "#2f6df6",
}: BackdropBlurModalProps) {
const [open, setOpen] = useState(false);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.stage} style={vars}>
<div className={styles.page}>
<div className={`${styles.line} ${styles.lineA}`} />
<div className={`${styles.line} ${styles.lineB}`} />
<button type="button" className={styles.trigger} onClick={() => setOpen(true)}>
{triggerLabel}
</button>
</div>
<div
className={open ? `${styles.overlay} ${styles.isOpen}` : styles.overlay}
onClick={() => setOpen(false)}
/>
<div
className={open ? `${styles.modal} ${styles.isOpen}` : styles.modal}
role="dialog"
aria-modal="true"
>
<h2>{title}</h2>
<p>{description}</p>
<button type="button" className={styles.confirm} onClick={() => setOpen(false)}>
{confirmLabel}
</button>
</div>
</div>
);
}04 Làm mờ nền — Diện tích đo được 100.0% · khung hình chuyển động 8/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · 9 hiệu ứng css modal animation gọn nhẹ
Bài viết chứa demo này
9 hiệu ứng css modal animation gọn nhẹMột css modal animation là chuyển động khi hộp thoại xuất hiện và biến mất, thay vì bật thẳng từ ẩn sang hiện. Chín kiểu Xem tất cả demo trong bài này


