09Modal xếp chồng
Mở modal thứ hai thì modal đầu lùi ra sau và nhỏ lại.
Dùng choForm nhiều bước, chi tiết trong chi tiết
0.95어둡게3단

<main class="stage is-demo">
<div class="overlay"></div>
<div class="modal modal--1" role="dialog" aria-modal="true" aria-labelledby="t9a">
<h2 id="t9a">Nhập địa chỉ giao hàng</h2>
<p>Nhập địa chỉ rồi tìm mã bưu điện.</p>
<button class="modal__next" type="button">Tìm mã bưu điện</button>
</div>
<div class="modal modal--2" role="dialog" aria-modal="true" aria-labelledby="t9b">
<h2 id="t9b">Tìm mã bưu điện</h2>
<p>Tìm theo tên đường sẽ nhanh hơn.</p>
<button class="modal__back" type="button">Chọn địa chỉ này</button>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-enter; // 두 번째 모달이 뜨는 시간
$easing: $ease-spring;
$color: $subject-blue; // 강조색(버튼)
@include stage($stage-ink);
.stage { position: relative; }
.overlay {
position: fixed; inset: 0;
background: rgba($stage-ink, .35);
transition: background $duration $easing;
}
.modal {
position: fixed; left: 50%; top: 50%;
width: min(78vw, 300px);
padding: clamp(12px, 4.5vmin, 22px);
border-radius: $r-card; background: $stage-paper; color: $stage-ink;
box-shadow: $shadow-raised;
h2 { margin: 0 0 6px; font-size: clamp(13px, 4.2vmin, 17px); }
p { margin: 0 0 14px; font-size: clamp(10px, 3.2vmin, 13px); color: rgba($stage-ink, .65); }
}
.modal__next, .modal__back {
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;
}
// 1번 모달: 평소엔 정면, 2번이 열리면 뒤로 물러나 작아진다
.modal--1 {
transform: translate(-50%, -50%);
transition: transform $duration $easing, opacity $duration $easing;
}
// 2번 모달: 평소엔 숨어 있다가 열리면 앞으로
.modal--2 {
transform: translate(-50%, -50%) translateY(10px) scale(.9);
opacity: 0; pointer-events: none;
transition: transform $duration $easing, opacity $duration $easing;
}
.stage.is-stacked .overlay { background: rgba($stage-ink, .6); }
.stage.is-stacked .modal--1 { transform: translate(-50%, calc(-50% - 16px)) scale(.95); opacity: .55; }
.stage.is-stacked .modal--2 { transform: translate(-50%, -50%) translateY(0) scale(1); opacity: 1; pointer-events: auto; }
.stage.is-demo .overlay { animation: overlay-loop $dur-loop ease-in-out infinite; }
.stage.is-demo .modal--1 { animation: back-loop $dur-loop $easing infinite; }
.stage.is-demo .modal--2 { animation: front-loop $dur-loop $easing infinite; }
@keyframes overlay-loop {
0%, 40% { background: rgba($stage-ink, .35); }
55%, 80% { background: rgba($stage-ink, .6); }
95%, 100% { background: rgba($stage-ink, .35); }
}
@keyframes back-loop {
0%, 40% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
55%, 80% { transform: translate(-50%, calc(-50% - 16px)) scale(.95); opacity: .55; }
95%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
}
@keyframes front-loop {
0%, 40% { transform: translate(-50%, -50%) translateY(10px) scale(.9); opacity: 0; }
55%, 80% { transform: translate(-50%, -50%) translateY(0) scale(1); opacity: 1; }
95%, 100% { transform: translate(-50%, -50%) translateY(10px) scale(.9); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .overlay, .stage.is-demo .modal--1, .stage.is-demo .modal--2 { animation: none; }
.overlay, .modal--1, .modal--2 { 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: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.stage {
position: relative;
}
.overlay {
position: fixed;
inset: 0;
background: rgba(23, 20, 26, 0.35);
transition: background 600ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.modal {
position: fixed;
left: 50%;
top: 50%;
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);
}
.modal h2 {
margin: 0 0 6px;
font-size: clamp(13px, 4.2vmin, 17px);
}
.modal p {
margin: 0 0 14px;
font-size: clamp(10px, 3.2vmin, 13px);
color: rgba(23, 20, 26, 0.65);
}
.modal__next, .modal__back {
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;
}
.modal--1 {
transform: translate(-50%, -50%);
transition: transform 600ms cubic-bezier(0.2, 0.8, 0.2, 1), opacity 600ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.modal--2 {
transform: translate(-50%, -50%) translateY(10px) scale(0.9);
opacity: 0;
pointer-events: none;
transition: transform 600ms cubic-bezier(0.2, 0.8, 0.2, 1), opacity 600ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.stage.is-stacked .overlay {
background: rgba(23, 20, 26, 0.6);
}
.stage.is-stacked .modal--1 {
transform: translate(-50%, calc(-50% - 16px)) scale(0.95);
opacity: 0.55;
}
.stage.is-stacked .modal--2 {
transform: translate(-50%, -50%) translateY(0) scale(1);
opacity: 1;
pointer-events: auto;
}
.stage.is-demo .overlay {
animation: overlay-loop 2s ease-in-out infinite;
}
.stage.is-demo .modal--1 {
animation: back-loop 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.stage.is-demo .modal--2 {
animation: front-loop 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes overlay-loop {
0%, 40% {
background: rgba(23, 20, 26, 0.35);
}
55%, 80% {
background: rgba(23, 20, 26, 0.6);
}
95%, 100% {
background: rgba(23, 20, 26, 0.35);
}
}
@keyframes back-loop {
0%, 40% {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
}
55%, 80% {
transform: translate(-50%, calc(-50% - 16px)) scale(0.95);
opacity: 0.55;
}
95%, 100% {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
}
}
@keyframes front-loop {
0%, 40% {
transform: translate(-50%, -50%) translateY(10px) scale(0.9);
opacity: 0;
}
55%, 80% {
transform: translate(-50%, -50%) translateY(0) scale(1);
opacity: 1;
}
95%, 100% {
transform: translate(-50%, -50%) translateY(10px) scale(0.9);
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .overlay, .stage.is-demo .modal--1, .stage.is-demo .modal--2 {
animation: none;
}
.overlay, .modal--1, .modal--2 {
transition-duration: 1ms;
}
}(function () {
var stage = document.querySelector('.stage');
var next = document.querySelector('.modal__next');
var back = document.querySelector('.modal__back');
next.addEventListener('click', function () { stage.classList.remove('is-demo'); stage.classList.add('is-stacked'); });
back.addEventListener('click', function () { stage.classList.remove('is-stacked'); });
})();import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./StackedModals.module.scss";
export type StackedModalsProps = {
firstTitle: string;
firstDescription: string;
nextLabel: string;
secondTitle: string;
secondDescription: string;
backLabel: string;
/** 두 번째 모달이 뜨는 시간 (기본 600ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function StackedModals({
firstTitle,
firstDescription,
nextLabel,
secondTitle,
secondDescription,
backLabel,
duration = 600,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#2f6df6",
}: StackedModalsProps) {
const [stacked, setStacked] = useState(false);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.stage} style={vars}>
<div className={stacked ? `${styles.overlay} ${styles.isStacked}` : styles.overlay} />
<div className={stacked ? `${styles.modal} ${styles.back}` : styles.modal}>
<h2>{firstTitle}</h2>
<p>{firstDescription}</p>
<button type="button" className={styles.next} onClick={() => setStacked(true)}>
{nextLabel}
</button>
</div>
<div className={stacked ? `${styles.modal2} ${styles.isOpen}` : styles.modal2}>
<h2>{secondTitle}</h2>
<p>{secondDescription}</p>
<button type="button" className={styles.back2} onClick={() => setStacked(false)}>
{backLabel}
</button>
</div>
</div>
);
}09 Modal xếp chồng — Diện tích đo được 28.221% · 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


