08Dùng thuộc tính popover gốc
Dùng thuộc tính popover và ::backdrop, trình duyệt tự quản lý việc mở/đóng, click ra ngoài và focus mà không cần toggle bằng JS.
Dùng choMenu hành động nhẹ, trang không muốn tăng thêm JS bundle
popover::backdropJS 없음

<main class="stage">
<div class="menu">
<button class="menu__trigger" type="button" popovertarget="menu-panel">Tùy chọn</button>
<ul class="menu__panel" id="menu-panel" popover>
<li><a href="#">Chia sẻ</a></li>
<li><a href="#">Đánh dấu</a></li>
<li><a href="#">Báo cáo</a></li>
</ul>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base; // 내용이 접혔다 펴지는 시간
$easing: $ease-pop; // 살짝 튀는 이징
$color: $subject-blue-deep; // 트리거 버튼 색 (크림 글자가 얹히므로 진한 파랑)
@include stage($stage-paper);
.menu {
position: relative;
font-weight: 500;
font-size: clamp(13px, 3.6vw, 16px);
line-height: 1.4;
font-family: inherit;
}
.menu__trigger {
border: 0;
border-radius: $r-control;
padding: clamp(9px, 3vw, 13px) clamp(16px, 4vw, 22px);
font-weight: 700;
font-family: inherit;
color: $subject-cream;
background: $color;
cursor: pointer;
}
.menu__panel {
width: max-content;
max-width: min(150px, 70vw);
margin: 0;
padding: 6px;
list-style: none;
border: 0;
border-radius: $r-card;
background: $subject-cream;
box-shadow: $shadow-raised;
transform: scale(.9);
opacity: 0;
}
.menu__panel li a {
display: block;
padding: 8px 12px;
border-radius: $r-control - 4px;
color: $stage-ink;
text-decoration: none;
}
.menu__panel li a:hover { background: rgba($stage-ink, .06); }
.menu__panel::backdrop {
background: rgba($stage-ink, .18);
}
// popover 는 열어 둔 채, 안쪽 내용만 2초 주기로 접었다 편다 (transform · opacity 만 움직인다)
.menu__panel:popover-open { animation: pop-loop $dur-loop $easing infinite; }
@keyframes pop-loop {
0%, 30% { transform: scale(.9); opacity: 0; }
45%, 80% { transform: scale(1); opacity: 1; }
95%, 100% { transform: scale(.9); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.menu__panel { transition: none !important; }
.menu__panel:popover-open { animation: none; transform: scale(1); opacity: 1; }
}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);
}
.menu {
position: relative;
font-weight: 500;
font-size: clamp(13px, 3.6vw, 16px);
line-height: 1.4;
font-family: inherit;
}
.menu__trigger {
border: 0;
border-radius: 12px;
padding: clamp(9px, 3vw, 13px) clamp(16px, 4vw, 22px);
font-weight: 700;
font-family: inherit;
color: #fff7e6;
background: #0a4fe8;
cursor: pointer;
}
.menu__panel {
width: max-content;
max-width: min(150px, 70vw);
margin: 0;
padding: 6px;
list-style: none;
border: 0;
border-radius: 16px;
background: #fff7e6;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
transform: scale(0.9);
opacity: 0;
}
.menu__panel li a {
display: block;
padding: 8px 12px;
border-radius: 8px;
color: #17141a;
text-decoration: none;
}
.menu__panel li a:hover {
background: rgba(23, 20, 26, 0.06);
}
.menu__panel::backdrop {
background: rgba(23, 20, 26, 0.18);
}
.menu__panel:popover-open {
animation: pop-loop 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
@keyframes pop-loop {
0%, 30% {
transform: scale(0.9);
opacity: 0;
}
45%, 80% {
transform: scale(1);
opacity: 1;
}
95%, 100% {
transform: scale(0.9);
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.menu__panel {
transition: none !important;
}
.menu__panel:popover-open {
animation: none;
transform: scale(1);
opacity: 1;
}
}// popover 는 열려야 화면에 그려진다. 열어 둔 채 안쪽 내용만 2초 주기로 접었다 편다.
// 트리거는 popovertarget 하나로 실제 열고 닫기·바깥 클릭·Esc 를 브라우저가 대신 처리한다.
document.getElementById('menu-panel').showPopover();import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./PopoverNativeDropdown.module.scss";
export type PopoverNativeDropdownProps = {
triggerLabel: string;
items: string[];
/** 내용이 접혔다 펴지는 시간 (기본 300ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function PopoverNativeDropdown({
triggerLabel,
items,
duration = 300,
easing = "cubic-bezier(.2,1.4,.4,1)",
color = "#2f6df6",
}: PopoverNativeDropdownProps) {
const [open, setOpen] = useState(false);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.menu} style={vars}>
<button type="button" className={styles.trigger} aria-expanded={open} onClick={() => setOpen((v) => !v)}>
{triggerLabel}
</button>
{/* React 는 showPopover() 를 ref 로 직접 불러야 해서 상태로 대신한다. 실제 zip 의 vanilla 판은 popover 속성 + popovertarget 을 그대로 쓴다 */}
<ul className={open ? `${styles.panel} ${styles.panelOpen}` : styles.panel}>
{items.map((label) => (
<li key={label}><a href="#">{label}</a></li>
))}
</ul>
</div>
);
}08 Dùng thuộc tính popover gốc — Diện tích đo được 8.969% · khung hình chuyển động 6/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · 9 css dropdown menu animation — không JS, chỉ CSS
Bài viết chứa demo này
9 css dropdown menu animation — không JS, chỉ CSSMột css dropdown menu animation là chuyển động khi menu điều hướng ẩn hiện, dựng bằng transition hoặc keyframes thuần CS Xem tất cả demo trong bài này


