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
.listViewtrê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).