Giới thiệu
Expanding Grid (hay Accordion Grid) cho phép người dùng click vào một item trong lưới để mở rộng hiển thị chi tiết, đẩy các item khác ra xa. Phù hợp cho portfolio, team, FAQ, product features.
Ví dụ hoàn chỉnh: 3 cột → Mở rộng full width
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Expanding Grid</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; background: #fafafa; padding: 2rem; }
.gridContainer {
max-width: 1000px; margin: 0 auto;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.gridItem {
background: white; border-radius: 12px; overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative; z-index: 1;
}
.gridItem:hover { transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0,0,0,0.12); }
.gridItem.expanded {
grid-column: 1 / -1; /* Chiếm toàn bộ rộng */
z-index: 10;
transform: none;
box-shadow: 0 20px 40px rgba(0,0,0,0.15);
}
.itemPreview {
padding: 2rem; text-align: center; min-height: 200px;
display: flex; flex-direction: column; align-items: center; justify-content: center;
}
.itemPreview h3 { color: #2c3e50; margin-bottom: 0.5rem; }
.itemPreview p { color: #666; font-size: 0.95rem; }
.itemDetail {
display: none; padding: 2rem; border-top: 1px solid #f0f0f0;
animation: slideDown 0.3s ease;
}
.gridItem.expanded .itemDetail { display: block; }
@keyframes slideDown {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: none; }
}
.closeBtn {
position: absolute; top: 1rem; right: 1rem;
width: 36px; height: 36px; border-radius: 50%;
background: #f5f5f5; border: none; cursor: pointer;
display: flex; align-items: center; justify-content: center;
font-size: 1.5rem; color: #666; transition: background 0.2s;
}
.closeBtn:hover { background: #e0e0e0; color: #333; }
/* Màu cho từng item */
.gridItem:nth-child(1) .itemPreview { background: linear-gradient(135deg, #ff6b6b, #ee5a5a); color: white; }
.gridItem:nth-child(2) .itemPreview { background: linear-gradient(135deg, #4ecdc4, #3dbdb5); color: white; }
.gridItem:nth-child(3) .itemPreview { background: linear-gradient(135deg, #a66cfe, #9055ff); color: white; }
.gridItem:nth-child(1) .itemPreview h3,
.gridItem:nth-child(2) .itemPreview h3,
.gridItem:nth-child(3) .itemPreview h3 { color: white; }
.gridItem:nth-child(1) .itemPreview p,
.gridItem:nth-child(2) .itemPreview p,
.gridItem:nth-child(3) .itemPreview p { color: rgba(255,255,255,0.9); }
/* Responsive */
@media (max-width: 768px) {
.gridContainer { grid-template-columns: 1fr; }
.gridItem.expanded { grid-column: auto; }
}
</style>
</head>
<body>
<h1 style="text-align:center; margin-bottom:0.5rem; color:#2c3e50;">Expanding Grid</h1>
<p style="text-align:center; color:#666; margin-bottom:2rem;">Click vào card để mở rộng xem chi tiết</p>
<div class="gridContainer" id="gridContainer">
<div class="gridItem" data-id="1">
<div class="itemPreview">
<h3>Dự án Alpha</h3>
<p>Click để xem chi tiết</p>
</div>
<div class="itemDetail">
<button class="closeBtn" aria-label="Đóng">×</button>
<h3>Dự án Alpha - Chi tiết</h3>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.</p>
<p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident.</p>
<a href="#" style="display:inline-block; margin-top:1rem; color:#ff6b6b; font-weight:600;">Xem dự án →</a>
</div>
</div>
<div class="gridItem" data-id="2">
<div class="itemPreview">
<h3>Dự án Beta</h3>
<p>Click để xem chi tiết</p>
</div>
<div class="itemDetail">
<button class="closeBtn" aria-label="Đóng">×</button>
<h3>Dự án Beta - Chi tiết</h3>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<a href="#" style="display:inline-block; margin-top:1rem; color:#4ecdc4; font-weight:600;">Xem dự án →</a>
</div>
</div>
<div class="gridItem" data-id="3">
<div class="itemPreview">
<h3>Dự án Gamma</h3>
<p>Click để xem chi tiết</p>
</div>
<div class="itemDetail">
<button class="closeBtn" aria-label="Đóng">×</button>
<h3>Dự án Gamma - Chi tiết</h3>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<a href="#" style="display:inline-block; margin-top:1rem; color:#a66cfe; font-weight:600;">Xem dự án →</a>
</div>
</div>
</div>
<script>
(function() {
const container = document.getElementById('gridContainer');
const items = container.querySelectorAll('.gridItem');
let expandedItem = null;
items.forEach(item => {
const preview = item.querySelector('.itemPreview');
const closeBtn = item.querySelector('.closeBtn');
preview.addEventListener('click', () => {
if (expandedItem === item) return; // Already expanded
// Close previous
if (expandedItem) {
expandedItem.classList.remove('expanded');
}
// Open current
item.classList.add('expanded');
expandedItem = item;
// Scroll into view smoothly
item.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
});
closeBtn.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent bubbling to preview
item.classList.remove('expanded');
if (expandedItem === item) expandedItem = null;
});
});
// Close on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && expandedItem) {
expandedItem.classList.remove('expanded');
expandedItem = null;
}
});
// Close on click outside
document.addEventListener('click', (e) => {
if (expandedItem && !expandedItem.contains(e.target)) {
expandedItem.classList.remove('expanded');
expandedItem = null;
}
});
})();
</script>
</body>
</html>
Giải thích kỹ thuật cốt lõi
CSS: Grid Column Span
.gridItem.expanded { grid-column: 1 / -1; }: Mở rộng từ cột 1 đến cột cuối (-1) - chiếm toàn bộ hàng.z-index: 10: Đưa item mở rộng lên trên các item khác.display: none/blocktrên.itemDetail: Ẩn/hiện nội dung chi tiết.- Animation:
@keyframes slideDowncho hiệu ứng mượt.
JavaScript: State Management
- Chỉ cho phép một item mở rộng tại một thời điểm (accordion behavior).
scrollIntoView(): Cuộn mượt đến item khi mở rộng (quantr quan trọng trên mobile).- Đóng bằng: Nút close, phím Escape, click ra ngoài.
e.stopPropagation()trên nút close để không trigger mở lại.
Biến thể: Expand trong chỗ (In-place expand)
Thay vì span full width, chỉ tăng chiều cao item:
.gridItem.expanded .itemDetail { display: block; }
/* Không cần grid-column: 1/-1, không cần z-index cao */
Phù hợp cho FAQ, danh sách sản phẩm có mô tả ngắn.
Performance & Accessibility
- Dùng
transformthay vì thay đổi width/height để tránh layout thrashing. - Nội dung detail nên lazy-load (chỉ render khi expand) nếu nặng.
- ARIA:
aria-expandedtrên item,aria-controlstrỏ đến detail panel. - Focus management: Khi expand, focus vào close button hoặc heading đầu tiên trong detail.