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

Cách tạo List/Grid View Toggle (Chuyển đổi xem Danh sách/Lưới) với CSS và JavaScript

GiỐi thiệu

Cho phép người dùng chuyển đổi giữa chế độ xem Danh sách (List - 1 cột) và Lưới (Grid - 2 cột trở lên) là tính năng UX tốt cho trang sản phẩm, portfolio, blog archive.

Ví dụ hoàn chỉnh: Vanilla JS + CSS Grid

<!DOCTYPE html>
<html lang="vi">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>List Grid View Toggle</title>
    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body { font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; background: #fafafa; padding: 2rem; }
        
        .controls {
            max-width: 1000px; margin: 0 auto 2rem;
            display: flex; gap: 8px; justify-content: center;
        }
        .viewBtn {
            padding: 0.7rem 1.5rem; border: 2px solid #e0e0e0; background: white;
            border-radius: 8px; cursor: pointer; font-weight: 600; color: #555;
            display: flex; align-items: center; gap: 6px; transition: all 0.2s;
        }
        .viewBtn:hover { border-color: #8e24aa; color: #8e24aa; }
        .viewBtn.active { background: #8e24aa; border-color: #8e24aa; color: white; }
        .viewBtn svg { width: 20px; height: 20px; }
        
        .itemsGrid {
            max-width: 1000px; margin: 0 auto;
            display: grid;
            grid-template-columns: repeat(2, 1fr); /* Default: Grid 2 cột */
            gap: 24px;
            transition: grid-template-columns 0.3s ease;
        }
        
        /* List View: 1 cột, layout ngang */
        .itemsGrid.listView {
            grid-template-columns: 1fr;
        }
        .itemsGrid.listView .item {
            grid-template-columns: 200px 1fr; /* Hình ảnh fixed, content flex */
            text-align: left;
            padding: 1.5rem;
        }
        .itemsGrid.listView .itemImage {
            width: 200px; height: 150px; border-radius: 8px;
        }
        .itemsGrid.listView .itemContent { padding-left: 1.5rem; display: flex; flex-direction: column; justify-content: center; }
        
        /* Grid View: 2 cột, layout dọc */
        .itemsGrid:not(.listView) .item {
            grid-template-columns: 1fr;
            text-align: center;
            padding: 1.5rem;
        }
        .itemsGrid:not(.listView) .itemImage {
            width: 100%; height: 200px; border-radius: 8px 8px 0 0; margin-bottom: 1rem;
        }
        .itemsGrid:not(.listView) .itemContent { padding: 0 1rem 1rem; }
        
        .item {
            background: white; border-radius: 12px; overflow: hidden;
            box-shadow: 0 4px 12px rgba(0,0,0,0.06);
            display: grid; gap: 1rem;
            transition: transform 0.2s, box-shadow 0.2s;
        }
        .item:hover { transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0,0,0,0.1); }
        .itemImage { object-fit: cover; background: #f0f0f0; }
        .itemTitle { font-size: 1.2rem; color: #2c3e50; font-weight: 700; }
        .itemDesc { color: #666; font-size: 0.95rem; line-height: 1.6; }
        
        @media (max-width: 600px) {
            .itemsGrid { grid-template-columns: 1fr; }
            .itemsGrid.listView .item { grid-template-columns: 1fr; }
            .itemsGrid.listView .itemImage { width: 100%; height: 180px; border-radius: 8px 8px 0 0; }
            .itemsGrid.listView .itemContent { padding: 1rem 0 0; }
        }
    </style>
</head>
<body>
    <h1 style="text-align:center; margin-bottom:0.5rem; color:#2c3e50;">List / Grid View Toggle</h1>
    <p style="text-align:center; color:#666; margin-bottom:1.5rem;">Nhấn nút để chuyển đổi chế độ xem</p>
    
    <div class="controls" role="group" aria-label="View mode">
        <button class="viewBtn active" data-view="grid" aria-pressed="true">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
            Lưới
        </button>
        <button class="viewBtn" data-view="list" aria-pressed="false">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
            Danh sách
        </button>
    </div>
    
    <div class="itemsGrid" id="itemsGrid">
        <article class="item">
            <img class="itemImage" src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=300&fit=crop" alt="Item 1">
            <div class="itemContent">
                <h3 class="itemTitle">Sản phẩm 1</h3>
                <p class="itemDesc">Mô tả ngắn gọn về sản phẩm này. Chất lượng cao, giá cả hợp lý.</p>
            </div>
        </article>
        <article class="item">
            <img class="itemImage" src="https://images.unsplash.com/photo-1544551763-46a013bb70d5?w=400&h=300&fit=crop" alt="Item 2">
            <div class="itemContent">
                <h3 class="itemTitle">Sản phẩm 2</h3>
                <p class="itemDesc">Mô tả ngắn gọn về sản phẩm này. Thiết kế hiện đại, bền bỉ.</p>
            </div>
        </article>
        <article class="item">
            <img class="itemImage" src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=400&h=300&fit=crop" alt="Item 3">
            <div class="itemContent">
                <h3 class="itemTitle">Sản phẩm 3</h3>
                <p class="itemDesc">Mô tả ngắn gọn về sản phẩm này. Đa dạng mẫu mã, màu sắc.</p>
            </div>
        </article>
        <article class="item">
            <img class="itemImage" src="https://images.unsplash.com/photo-1523830213227-f76e0415a976?w=400&h=300&fit=crop" alt="Item 4">
            <div class="itemContent">
                <h3 class="itemTitle">Sản phẩm 4</h3>
                <p class="itemDesc">Mô tả ngắn gọn về sản phẩm này. Giao hàng nhanh toàn quốc.</p>
            </div>
        </article>
    </div>

    <script>
        (function() {
            const grid = document.getElementById('itemsGrid');
            const buttons = document.querySelectorAll('.viewBtn');
            
            buttons.forEach(btn => {
                btn.addEventListener('click', () => {
                    const view = btn.dataset.view;
                    
                    // Update buttons
                    buttons.forEach(b => {
                        b.classList.toggle('active', b === btn);
                        b.setAttribute('aria-pressed', b === btn);
                    });
                    
                    // Toggle grid class
                    grid.classList.toggle('listView', view === 'list');
                    
                    // Save preference
                    localStorage.setItem('viewMode', view);
                });
            });
            
            // Restore preference
            const saved = localStorage.getItem('viewMode');
            if (saved) {
                const btn = document.querySelector(`[data-view="${saved}"]`);
                if (btn) btn.click();
            }
        })();
    </script>
</body>
</html>

Phân tích kỹ thuật

CSS: Grid Template Columns Toggle

  • Mặc định: grid-template-columns: repeat(2, 1fr) (Grid 2 cột).
  • Class .listView trên container: grid-template-columns: 1fr (1 cột).
  • Item layout thay đổi nhờ CSS descendant selector: .itemsGrid.listView .item { grid-template-columns: 200px 1fr; }.
  • Transition mượt mà trên grid-template-columns (hỗ trợ Chrome/Edge, Firefox chưa hỗ trợ animate grid-template).

JavaScript: State Management

  • Event delegation trên container nút bấm.
  • Toggle class + aria-pressed cho accessibility.
  • Lưu preference vào localStorage - ghi nhớ lựa chọn user.
  • Khôi phục state khi load trang.

Mở rộng: Grid 3/4 cột + List

.itemsGrid { grid-template-columns: repeat(4, 1fr); }
.itemsGrid.view-3 { grid-template-columns: repeat(3, 1fr); }
.itemsGrid.view-2 { grid-template-columns: repeat(2, 1fr); }
.itemsGrid.listView { grid-template-columns: 1fr; }

Thêm nút bấm cho 3/2 cột, data-view="3", "2", "list". JS thêm class tương ứng.

Accessibility Checklist

  • Nút bấm: role="group", aria-label, aria-pressed.
  • Focus visible: .viewBtn:focus-visible { outline: 2px solid #8e24aa; outline-offset: 2px; }.
  • Keyboard: Tab giữa các nút, Enter/Space để chọn.
  • Screen reader: Announce view change (cần ARIA live region cho phức tạp).