Patterns Medium
Bulk Actions
Multi-select grid pattern with a sticky bulk action bar for archive, tag, and delete flows.
Open in Lab
MCP
vanilla-js css
Targets: JS HTML
Code
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "Sora", system-ui, sans-serif;
background: #0b1221;
color: #e2e8f0;
padding-bottom: 5rem;
}
.shell {
width: min(960px, calc(100% - 2rem));
margin: 1.8rem auto;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
margin-bottom: 0.8rem;
}
.select-all {
color: #94a3b8;
font-size: 0.9rem;
display: flex;
gap: 0.4rem;
align-items: center;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
gap: 0.75rem;
}
.card {
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 12px;
background: #111c30;
padding: 0.8rem;
display: grid;
gap: 0.55rem;
}
.card-top {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.8rem;
color: #93c5fd;
}
.card h3 {
margin: 0;
font-size: 1rem;
}
.card p {
margin: 0;
color: #94a3b8;
font-size: 0.85rem;
}
.bulk-bar {
position: fixed;
left: 50%;
bottom: 1rem;
transform: translateX(-50%);
width: min(760px, calc(100% - 2rem));
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
background: rgba(6, 10, 19, 0.95);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.7rem 0.9rem;
}
.bulk-bar button {
border: 1px solid rgba(255, 255, 255, 0.16);
background: transparent;
color: #dbeafe;
border-radius: 8px;
padding: 0.35rem 0.6rem;
}
.bulk-bar .danger {
border-color: rgba(248, 113, 113, 0.6);
color: #fecaca;
}(() => {
let items = Array.from({ length: 12 }, (_, index) => ({
id: index + 1,
name: `Asset ${index + 1}`,
detail: `Design artifact ${index + 1}`,
}));
const selected = new Set();
const grid = document.getElementById("grid");
const toggleAll = document.getElementById("toggle-all");
const bulkBar = document.getElementById("bulk-bar");
const selectedCount = document.getElementById("selected-count");
const archiveBtn = document.getElementById("archive");
const removeBtn = document.getElementById("remove");
const render = () => {
grid.innerHTML = "";
for (const item of items) {
const checked = selected.has(item.id);
const card = document.createElement("article");
card.className = "card";
card.innerHTML = `
<div class="card-top">
<span>#${item.id}</span>
<label><input data-select="${item.id}" type="checkbox" ${checked ? "checked" : ""} /> Select</label>
</div>
<h3>${item.name}</h3>
<p>${item.detail}</p>
`;
grid.appendChild(card);
}
const count = selected.size;
selectedCount.textContent = `${count} selected`;
bulkBar.hidden = count === 0;
toggleAll.checked = items.length > 0 && count === items.length;
toggleAll.indeterminate = count > 0 && count < items.length;
};
grid.addEventListener("change", (event) => {
const target = event.target;
if (!(target instanceof HTMLInputElement)) return;
const id = target.getAttribute("data-select");
if (!id) return;
const numericId = Number(id);
if (target.checked) {
selected.add(numericId);
} else {
selected.delete(numericId);
}
render();
});
toggleAll.addEventListener("change", () => {
if (toggleAll.checked) {
selected.clear();
for (const item of items) selected.add(item.id);
} else {
selected.clear();
}
render();
});
archiveBtn.addEventListener("click", () => {
items = items.map((item) => {
if (!selected.has(item.id)) return item;
return { ...item, detail: `${item.detail} (archived)` };
});
selected.clear();
render();
});
removeBtn.addEventListener("click", () => {
items = items.filter((item) => !selected.has(item.id));
selected.clear();
render();
});
render();
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bulk Actions</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="shell">
<header>
<h1>Bulk Actions Grid</h1>
<label class="select-all"><input id="toggle-all" type="checkbox" /> Select all</label>
</header>
<section id="grid" class="grid" aria-label="Selectable cards"></section>
</main>
<footer id="bulk-bar" class="bulk-bar" hidden>
<strong id="selected-count">0 selected</strong>
<div>
<button id="archive" type="button">Archive</button>
<button id="remove" type="button" class="danger">Delete</button>
</div>
</footer>
<script src="script.js"></script>
</body>
</html>Bulk Actions
A reusable selection pattern for card or list layouts where users apply actions to many items at once.
Features
- Per-item checkbox selection
- Select-all support
- Sticky bulk bar with count
- Archive and delete bulk handlers
Notes
This pattern uses a card grid instead of table rows to stay distinct from data-table.