Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> CSS

Cách tạo Expanding Grid (Lưới mở rộng) với CSS và JavaScript

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/block trên .itemDetail: Ẩn/hiện nội dung chi tiết.
  • Animation: @keyframes slideDown cho 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 transform thay 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-expanded trên item, aria-controls trỏ đến detail panel.
  • Focus management: Khi expand, focus vào close button hoặc heading đầu tiên trong detail.