Các menu bật lên, không giống như các chú giải công cụ xuất hiện khi di chuột, mở khi nhấp chuột và được sử dụng để hiển thị danh sách các hành động (chẳng hạn như chỉnh sửa hoặc xóa) cho người dùng. Nó được thiết kế chú trọng đến khả năng truy cập, chẳng hạn như quản lý tiêu điểm và xử lý đóng không nhấp chuột.
Hiển thị tùy chọn: Lý tưởng cho giao diện người dùng hiển thị nhiều hành động khác nhau (chỉnh sửa, xóa, chia sẻ, v.v.) bằng cách nhấp vào biểu tượng menu (…) hoặc nút.
Cân nhắc về khả năng truy cập: Sử dụng các thuộc tính aria-expanded và aria-haspopup để hỗ trợ quản lý ẩn và tập trung bằng phím Escape.
Hoạt ảnh mượt mà: Để tránh chuyển đổi hiển thị không tự nhiên, chúng tôi đã triển khai quá trình chuyển đổi kết hợp trượt theo hướng trục Y và làm mờ dần.
Clear Hierarchy: Bóng (box-shadow) được sử dụng để tạo cảm giác nổi và tách biệt rõ ràng nội dung với nội dung nền.
Vận hành an toàn: Bao gồm quy trình đóng menu bằng cách nhấn vào một khu vực ngoài phạm vi nhấp chuột (Nhấp vào bên ngoài), giúp thao tác trực quan trên cả thiết bị di động và PC.
Nêu bật các hành động phá hoại: Chúng tôi sử dụng văn bản màu đỏ (Nguy hiểm) cho các hành động không thể hoàn tác, chẳng hạn như xóa, để giảm thiểu lỗi của người dùng.
Nếu bạn muốn thay đổi độ rộng của menu, vui lòng điều chỉnh width của .popover-menu.
Nếu bạn muốn hướng mở ở dưới bên phải hoặc dưới bên trái, bạn có thể dễ dàng tùy chỉnh bằng cách thay đổi thuộc tính transform và left của .popover-menu.
Bạn có thể đưa menu bật lên vào dự án của mình bằng mã bên dưới.
<div class="popover-wrapper">
<button class="popover-trigger" aria-haspopup="true" aria-expanded="false" id="popover-btn">
<span>hành động</span>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m6 9 6 6 6-6"/>
</svg>
</button>
<div class="popover-menu" role="menu" aria-labelledby="popover-btn">
<div class="popover-content">
<button class="popover-item" role="menuitem">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
Chỉnh sửa
</button>
<button class="popover-item" role="menuitem">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><path d="M21 9H3"/><path d="M9 21V9"/></svg>
trùng lặp
</button>
<div class="popover-divider"></div>
<button class="popover-item text-danger" role="menuitem">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
xóa
</button>
</div>
</div>
</div>
<style>
/* Đặt CSS vào đây */
.popover-wrapper {
position: relative;
display: inline-block;
}
.popover-trigger {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
color: #334155;
background-color: #ffffff;
border: 1px solid #cbd5e1;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.popover-trigger:hover {
background-color: #f1f5f9;
border-color: #94a3b8;
}
.popover-trigger:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
.popover-trigger svg {
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.popover-trigger.is-open svg {
transform: rotate(180deg);
}
.popover-menu {
position: absolute;
top: calc(100% + 8px);
left: 50%;
transform: translateX(-50%) translateY(-10px);
width: 200px;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
border: 1px solid #e2e8f0;
opacity: 0;
visibility: hidden;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
z-index: 50;
pointer-events: none;
}
.popover-wrapper.is-active .popover-menu {
opacity: 1;
visibility: visible;
transform: translateX(-50%) translateY(0);
pointer-events: auto;
}
.popover-content {
padding: 6px;
display: flex;
flex-direction: column;
gap: 2px;
}
.popover-item {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 8px 12px;
font-size: 14px;
color: #475569;
background: transparent;
border: none;
border-radius: 6px;
cursor: pointer;
text-align: left;
transition: background-color 0.15s ease, color 0.15s ease;
}
.popover-item:hover,
.popover-item:focus-visible {
background-color: #f1f5f9;
color: #0f172a;
outline: none;
}
.popover-item.text-danger {
color: #ef4444;
}
.popover-item.text-danger:hover,
.popover-item.text-danger:focus-visible {
background-color: #fef2f2;
color: #dc2626;
}
.popover-divider {
height: 1px;
background-color: #e2e8f0;
margin: 4px 0;
}
</style>
<script>
// Đặt JavaScript vào đây
document.addEventListener('DOMContentLoaded', () => {
const wrappers = document.querySelectorAll('.popover-wrapper');
wrappers.forEach(wrapper => {
const trigger = wrapper.querySelector('.popover-trigger');
const menu = wrapper.querySelector('.popover-menu');
if (!trigger || !menu) return;
const togglePopover = () => {
const isActive = wrapper.classList.contains('is-active');
if (isActive) {
closePopover();
} else {
openPopover();
}
};
const openPopover = () => {
wrapper.classList.add('is-active');
trigger.classList.add('is-open');
trigger.setAttribute('aria-expanded', 'true');
};
const closePopover = () => {
wrapper.classList.remove('is-active');
trigger.classList.remove('is-open');
trigger.setAttribute('aria-expanded', 'false');
};
trigger.addEventListener('click', togglePopover);
//Đóng bên ngoài vùng click
document.addEventListener('click', (e) => {
if (!wrapper.contains(e.target)) {
closePopover();
}
});
// Đóng bằng phím Escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && wrapper.classList.contains('is-active')) {
closePopover();
trigger.focus();
}
});
});
});
</script>