03Drawer bên cạnh
Panel trượt vào từ mép phải.
Dùng choGiỏ hàng, panel cài đặt, menu
320px왼쪽에서전체 폭

<main class="stage is-demo">
<button class="trigger" type="button">Giỏ hàng</button>
<div class="overlay"></div>
<div class="drawer" role="dialog" aria-modal="true" aria-labelledby="t3">
<h2 id="t3">Giỏ hàng (2)</h2>
<p>Tổng cộng 48.000₫. Phí giao hàng ở màn hình sau.</p>
<button class="drawer__close" type="button">Đóng</button>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-enter; // 패널이 밀려 들어오는 시간
$easing: $ease-spring; // 끝에서 살짝 튕긴다
$color: $subject-cream; // 강조색(버튼)
@include stage($stage-orange);
.stage { position: relative; }
.trigger {
border: 0; border-radius: $r-pill;
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-orange; background: $subject-cream;
cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.overlay {
position: fixed; inset: 0;
background: rgba($stage-ink, .5);
opacity: 0; pointer-events: none;
transition: opacity $duration $easing;
}
.drawer {
position: fixed; right: 0; top: 0; bottom: 0; left: auto;
transform: translateX(100%);
width: min(78vw, 260px);
box-sizing: border-box;
padding: clamp(14px, 5vmin, 24px);
background: $stage-paper; color: $stage-ink;
box-shadow: $shadow-raised;
pointer-events: none;
transition: transform $duration $easing;
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); }
}
.drawer__close {
border: 0; border-radius: $r-control; background: $stage-orange; color: $color;
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; pointer-events: auto; }
.stage.is-open .drawer { transform: translateX(0); pointer-events: auto; }
.stage.is-demo .overlay { animation: overlay-loop $dur-loop ease-in-out infinite; }
.stage.is-demo .drawer { animation: drawer-loop $dur-loop $easing infinite; }
@keyframes overlay-loop {
0% { opacity: 0; }
15% { opacity: 1; }
70% { opacity: 1; }
85% { opacity: 0; }
100% { opacity: 0; }
}
@keyframes drawer-loop {
0% { transform: translateX(100%); }
15% { transform: translateX(0); }
70% { transform: translateX(0); }
85% { transform: translateX(100%); }
100% { transform: translateX(100%); }
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .overlay, .stage.is-demo .drawer { animation: none; }
.overlay, .drawer { 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: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.stage {
position: relative;
}
.trigger {
border: 0;
border-radius: 999px;
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: #ff4d1f;
background: #fff7e6;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.overlay {
position: fixed;
inset: 0;
background: rgba(23, 20, 26, 0.5);
opacity: 0;
pointer-events: none;
transition: opacity 600ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.drawer {
position: fixed;
right: 0;
top: 0;
bottom: 0;
left: auto;
transform: translateX(100%);
width: min(78vw, 260px);
box-sizing: border-box;
padding: clamp(14px, 5vmin, 24px);
background: #ffffff;
color: #17141a;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
pointer-events: none;
transition: transform 600ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.drawer h2 {
margin: 0 0 6px;
font-size: clamp(13px, 4.2vmin, 17px);
}
.drawer p {
margin: 0 0 14px;
font-size: clamp(10px, 3.2vmin, 13px);
color: rgba(23, 20, 26, 0.65);
}
.drawer__close {
border: 0;
border-radius: 12px;
background: #ff4d1f;
color: #fff7e6;
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;
pointer-events: auto;
}
.stage.is-open .drawer {
transform: translateX(0);
pointer-events: auto;
}
.stage.is-demo .overlay {
animation: overlay-loop 2s ease-in-out infinite;
}
.stage.is-demo .drawer {
animation: drawer-loop 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes overlay-loop {
0% {
opacity: 0;
}
15% {
opacity: 1;
}
70% {
opacity: 1;
}
85% {
opacity: 0;
}
100% {
opacity: 0;
}
}
@keyframes drawer-loop {
0% {
transform: translateX(100%);
}
15% {
transform: translateX(0);
}
70% {
transform: translateX(0);
}
85% {
transform: translateX(100%);
}
100% {
transform: translateX(100%);
}
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .overlay, .stage.is-demo .drawer {
animation: none;
}
.overlay, .drawer {
transition-duration: 1ms;
}
}(function () {
var stage = document.querySelector('.stage');
var trigger = document.querySelector('.trigger');
var overlay = document.querySelector('.overlay');
var close = document.querySelector('.drawer__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 "./SideDrawer.module.scss";
export type SideDrawerProps = {
triggerLabel: string;
title: string;
description: string;
closeLabel: string;
/** 패널이 밀려 들어오는 시간 (기본 600ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function SideDrawer({
triggerLabel,
title,
description,
closeLabel,
duration = 600,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#fff7e6",
}: SideDrawerProps) {
const [open, setOpen] = useState(false);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.stage} style={vars}>
<button type="button" className={styles.trigger} onClick={() => setOpen(true)}>
{triggerLabel}
</button>
<div
className={open ? `${styles.overlay} ${styles.isOpen}` : styles.overlay}
onClick={() => setOpen(false)}
/>
<div
className={open ? `${styles.drawer} ${styles.isOpen}` : styles.drawer}
role="dialog"
aria-modal="true"
>
<h2>{title}</h2>
<p>{description}</p>
<button type="button" className={styles.close} onClick={() => setOpen(false)}>
{closeLabel}
</button>
</div>
</div>
);
}03 Drawer bên cạnh — Diện tích đo được 100.0% · khung hình chuyển động 9/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


