@use 'sprucecss/scss/spruce' as *;
.bulk-actions {
background-color: color('surface');
border: config('border-width-base', $display) solid color('border');
border-radius: radius('lg');
overflow: hidden;
&__header {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: spacer('xs') spacer('m');
justify-content: space-between;
min-block-size: 3.75rem;
padding: spacer('xs') spacer('m');
}
&__title {
font-size: font-size('h5');
margin: 0;
}
&__bar {
@include layout-cluster('xs');
border-radius: radius('md');
margin-inline-start: auto;
@include transition($property: 'background-color, padding');
}
&__count {
color: color('muted');
font-size: config('font-size-sm', $typography);
margin: 0;
.bulk-actions__bar--active & {
color: color('heading');
font-weight: 700;
margin-inline-end: spacer('xs');
}
&:empty {
display: none;
}
}
&__table {
margin: 0;
tr:has(.form-check__control:checked) {
background-color: color-alpha('primary', 0.03);
}
th[scope='row'] a {
font-weight: 700;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
&__check {
inline-size: 3.5rem;
min-inline-size: 0 !important;
padding-inline-start: spacer('m') !important;
.form-check__control {
display: block;
}
}
&__number {
font-variant-numeric: tabular-nums;
text-align: end;
}
&__status {
align-items: center;
display: inline-flex;
font-size: config('font-size-sm', $typography);
gap: spacer('xxs');
white-space: nowrap;
&::before {
aspect-ratio: 1;
background-color: color('muted');
border-radius: 50%;
content: '';
inline-size: 0.5rem;
}
&--success::before {
background-color: color('success', 'alert');
}
&--warning::before {
background-color: color('warning', 'alert');
}
}
}
[dir='rtl'] .bulk-actions__bar--active {
padding: spacer('xxs') spacer('s') spacer('xxs') spacer('xxs');
}
<form class="bulk-actions" action="#" method="post" data-bulk-actions>
<div class="bulk-actions__header">
<h3 class="bulk-actions__title" id="bulk-actions-title">Orders</h3>
<div class="bulk-actions__bar" role="toolbar" aria-label="Bulk actions" data-bulk-bar>
<p class="bulk-actions__count" data-bulk-count>Select orders to apply an action</p>
<button class="btn btn--sm btn--outline-secondary" type="submit" name="action" value="fulfill">Mark as fulfilled</button>
<button class="btn btn--sm btn--outline-secondary" type="submit" name="action" value="export">Export</button>
<button class="btn btn--sm btn--outline-danger" type="submit" name="action" value="archive">Archive</button>
<button class="btn btn--sm btn--ghost-secondary" type="button" data-bulk-clear hidden>Clear</button>
</div>
</div>
<p class="sr-only" aria-live="polite" data-bulk-live></p>
<div class="table-responsive">
<table class="table table--hover table--middle bulk-actions__table" aria-labelledby="bulk-actions-title">
<thead>
<tr>
<th class="bulk-actions__check" scope="col"><span class="form-check"><input class="form-check__control" type="checkbox" aria-label="Select all orders" data-bulk-all hidden></span></th>
<th scope="col">Order</th>
<th scope="col">Customer</th>
<th scope="col">Date</th>
<th class="bulk-actions__number" scope="col">Total</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody>
<tr>
<td class="bulk-actions__check"><span class="form-check"><input class="form-check__control" type="checkbox" name="orders[]" value="10482" aria-label="Select order #10482" data-bulk-item></span></td>
<th scope="row"><a href="#">#10482</a></th>
<td>Anna Smith</td>
<td><time datetime="2026-10-03">Oct 3</time></td>
<td class="bulk-actions__number">$186.84</td>
<td><span class="bulk-actions__status bulk-actions__status--warning">Unfulfilled</span></td>
</tr>
<tr>
<td class="bulk-actions__check"><span class="form-check"><input class="form-check__control" type="checkbox" name="orders[]" value="10481" aria-label="Select order #10481" data-bulk-item></span></td>
<th scope="row"><a href="#">#10481</a></th>
<td>Ben King</td>
<td><time datetime="2026-10-03">Oct 3</time></td>
<td class="bulk-actions__number">$89.00</td>
<td><span class="bulk-actions__status bulk-actions__status--warning">Unfulfilled</span></td>
</tr>
<tr>
<td class="bulk-actions__check"><span class="form-check"><input class="form-check__control" type="checkbox" name="orders[]" value="10480" aria-label="Select order #10480" data-bulk-item></span></td>
<th scope="row"><a href="#">#10480</a></th>
<td>Lea Moreau</td>
<td><time datetime="2026-10-02">Oct 2</time></td>
<td class="bulk-actions__number">$342.10</td>
<td><span class="bulk-actions__status bulk-actions__status--success">Fulfilled</span></td>
</tr>
<tr>
<td class="bulk-actions__check"><span class="form-check"><input class="form-check__control" type="checkbox" name="orders[]" value="10479" aria-label="Select order #10479" data-bulk-item></span></td>
<th scope="row"><a href="#">#10479</a></th>
<td>Jon Diaz</td>
<td><time datetime="2026-10-02">Oct 2</time></td>
<td class="bulk-actions__number">$54.50</td>
<td><span class="bulk-actions__status bulk-actions__status--warning">Unfulfilled</span></td>
</tr>
<tr>
<td class="bulk-actions__check"><span class="form-check"><input class="form-check__control" type="checkbox" name="orders[]" value="10478" aria-label="Select order #10478" data-bulk-item></span></td>
<th scope="row"><a href="#">#10478</a></th>
<td>Mia Chen</td>
<td><time datetime="2026-10-01">Oct 1</time></td>
<td class="bulk-actions__number">$129.00</td>
<td><span class="bulk-actions__status bulk-actions__status--muted">Refunded</span></td>
</tr>
</tbody>
</table>
</div>
</form>
(() => {
document.querySelectorAll('[data-bulk-actions]').forEach((form) => {
const all = form.querySelector('[data-bulk-all]');
const items = [...form.querySelectorAll('[data-bulk-item]')];
const bar = form.querySelector('[data-bulk-bar]');
const count = form.querySelector('[data-bulk-count]');
const clear = form.querySelector('[data-bulk-clear]');
const live = form.querySelector('[data-bulk-live]');
const actions = [...bar.querySelectorAll('button[type="submit"]')];
let last = null;
const update = (announce = true) => {
const selected = items.filter((item) => item.checked).length;
const text = selected ? `${selected} of ${items.length} selected` : 'No orders selected';
all.checked = selected === items.length;
all.indeterminate = selected > 0 && selected < items.length;
count.textContent = selected ? text : '';
bar.classList.toggle('bulk-actions__bar--active', selected > 0);
actions.forEach((button) => { button.hidden = !selected; });
clear.hidden = !selected;
if (announce) {
live.textContent = text;
}
};
all.hidden = false;
all.addEventListener('change', () => {
items.forEach((item) => { item.checked = all.checked; });
update();
});
items.forEach((item) => {
item.addEventListener('click', (event) => {
if (event.shiftKey && last) {
const [start, end] = [items.indexOf(last), items.indexOf(item)].sort((a, b) => a - b);
items.slice(start, end + 1).forEach((other) => { other.checked = item.checked; });
}
last = item;
update();
});
});
clear.addEventListener('click', () => {
items.forEach((item) => { item.checked = false; });
update();
all.focus();
});
update(false);
});
})();