@use 'sprucecss/scss/spruce' as *;
.mini-cart {
display: inline-block;
position: relative;
&__count {
background-color: color('primary');
border-radius: radius('pill');
color: color('primary-foreground', 'btn');
font-size: 0.75rem;
font-weight: 700;
line-height: 1;
min-inline-size: 1.5em;
padding: 0.25em 0.5em;
text-align: center;
}
&__panel {
background-color: color('surface');
border: config('border-width-base', $display) solid color('border');
border-radius: radius('lg');
box-shadow: shadow('xl');
inline-size: min(24rem, calc(100vi - #{spacer('m')} * 2));
inset-block-start: calc(100% + #{spacer('xs')});
inset-inline-end: 0;
position: absolute;
z-index: z-index('dropdown');
&:focus {
outline: none;
}
&:focus-visible {
outline: 2px solid color('focus-ring');
outline-offset: 2px;
}
@include motion-safe {
animation: mini-cart-in 0.2s ease-out;
}
}
&__header {
align-items: center;
border-block-end: config('border-width-base', $display) solid color('border');
display: flex;
justify-content: space-between;
padding: spacer('xs') spacer('xs') spacer('xs') spacer('m');
}
&__title {
font-size: font-size('h5');
margin: 0;
}
&__items {
@include clear-list;
max-block-size: 18rem;
overflow-y: auto;
padding-inline: spacer('m');
}
&__item {
align-items: center;
border-block-end: config('border-width-base', $display) solid color('border');
display: grid;
gap: spacer('s');
grid-template-columns: 3.5rem minmax(0, 1fr) auto auto;
margin: 0 !important;
padding-block: spacer('s');
&:last-child {
border-block-end: 0;
}
}
&__thumb {
aspect-ratio: 1;
border-radius: radius('md');
inline-size: 3.5rem;
object-fit: cover;
}
&__info {
display: grid;
font-size: config('font-size-sm', $typography);
line-height: config('line-height-md', $typography);
}
&__name {
color: color('heading');
font-weight: 700;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
&__meta {
color: color('muted');
}
&__price {
color: color('heading');
font-size: config('font-size-sm', $typography);
font-variant-numeric: tabular-nums;
font-weight: 700;
}
&__remove {
@include clear-btn;
border-radius: radius('sm');
color: color('muted');
display: inline-flex;
padding: spacer('xxs');
svg {
block-size: 1rem;
inline-size: 1rem;
}
&:hover {
color: color('danger', 'alert');
}
&:focus-visible {
outline: 2px solid color('focus-ring');
}
}
&__empty {
color: color('muted');
margin: 0;
padding: spacer('l') spacer('m');
text-align: center;
}
&__footer {
background-color: color('surface-muted');
border-block-start: config('border-width-base', $display) solid color('border');
border-end-end-radius: inherit;
border-end-start-radius: inherit;
padding: spacer('m');
@include layout-stack('xs');
}
&__subtotal {
color: color('heading');
display: flex;
font-size: config('font-size-lg', $typography);
justify-content: space-between;
margin: 0;
}
&__note {
color: color('muted');
font-size: config('font-size-sm', $typography);
hyphens: manual;
}
&__actions {
display: grid;
gap: spacer('xs');
grid-template-columns: 1fr 1fr;
padding-block-start: spacer('xs');
}
}
[dir='rtl'] .mini-cart__header {
padding: spacer('xs') spacer('m') spacer('xs') spacer('xs');
}
@keyframes mini-cart-in {
from {
opacity: 0;
translate: 0 -0.5rem;
}
}
<div class="mini-cart" data-mini-cart>
<a class="btn btn--outline-secondary mini-cart__toggle" href="#" aria-controls="mini-cart-panel" data-mini-cart-toggle>
<svg class="btn__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="8" cy="21" r="1"/><circle cx="19" cy="21" r="1"/><path d="M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12"/></svg>
Cart
<span class="mini-cart__count"><span data-mini-cart-count>4</span><span class="sr-only"> items</span></span>
</a>
<div class="mini-cart__panel" id="mini-cart-panel" role="region" aria-labelledby="mini-cart-title" tabindex="-1" hidden>
<header class="mini-cart__header">
<h2 class="mini-cart__title" id="mini-cart-title">Your cart</h2>
<button class="btn btn--ghost-secondary btn--icon btn--sm" type="button" data-mini-cart-close>
<svg class="btn__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M18 6 6 18M6 6l12 12"/></svg>
<span class="sr-only">Close the cart</span>
</button>
</header>
<ul class="mini-cart__items" role="list" data-mini-cart-items>
<li class="mini-cart__item" data-mini-cart-item data-price="89" data-quantity="1">
<img class="mini-cart__thumb" src="https://images.unsplash.com/photo-1553062407-98eeb64c6a62?auto=format&fit=crop&w=128&h=128&q=60" alt="" width="64" height="64" loading="lazy">
<div class="mini-cart__info">
<a class="mini-cart__name" href="#">Everyday Backpack</a>
<span class="mini-cart__meta">Charcoal · 22 L · Qty 1</span>
</div>
<span class="mini-cart__price">$89.00</span>
<button class="mini-cart__remove" type="button" data-mini-cart-remove>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M18 6 6 18M6 6l12 12"/></svg>
<span class="sr-only">Remove Everyday Backpack</span>
</button>
</li>
<li class="mini-cart__item" data-mini-cart-item data-price="42" data-quantity="2">
<img class="mini-cart__thumb" src="https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?auto=format&fit=crop&w=128&h=128&q=60" alt="" width="64" height="64" loading="lazy">
<div class="mini-cart__info">
<a class="mini-cart__name" href="#">Ceramic Pour-Over</a>
<span class="mini-cart__meta">White · Qty 2</span>
</div>
<span class="mini-cart__price">$84.00</span>
<button class="mini-cart__remove" type="button" data-mini-cart-remove>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M18 6 6 18M6 6l12 12"/></svg>
<span class="sr-only">Remove Ceramic Pour-Over</span>
</button>
</li>
<li class="mini-cart__item" data-mini-cart-item data-price="199" data-quantity="1">
<img class="mini-cart__thumb" src="https://images.unsplash.com/photo-1505740420928-5e560c06d30e?auto=format&fit=crop&w=128&h=128&q=60" alt="" width="64" height="64" loading="lazy">
<div class="mini-cart__info">
<a class="mini-cart__name" href="#">Wireless Headphones</a>
<span class="mini-cart__meta">Black · Qty 1</span>
</div>
<span class="mini-cart__price">$199.00</span>
<button class="mini-cart__remove" type="button" data-mini-cart-remove>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M18 6 6 18M6 6l12 12"/></svg>
<span class="sr-only">Remove Wireless Headphones</span>
</button>
</li>
</ul>
<p class="mini-cart__empty" data-mini-cart-empty hidden>Your cart is empty.</p>
<footer class="mini-cart__footer">
<p class="mini-cart__subtotal"><span>Subtotal</span> <strong data-mini-cart-subtotal>$372.00</strong></p>
<p class="mini-cart__note">Shipping and taxes are calculated at checkout.</p>
<div class="mini-cart__actions">
<a class="btn btn--outline-primary" href="#">View cart</a>
<a class="btn btn--primary" href="#" data-mini-cart-checkout>Checkout</a>
</div>
</footer>
<p class="sr-only" aria-live="polite" data-mini-cart-live></p>
</div>
</div>
(() => {
document.querySelectorAll('[data-mini-cart]').forEach((cart) => {
const toggle = cart.querySelector('[data-mini-cart-toggle]');
const panel = cart.querySelector('#' + toggle.getAttribute('aria-controls'));
const list = cart.querySelector('[data-mini-cart-items]');
const live = cart.querySelector('[data-mini-cart-live]');
const format = new Intl.NumberFormat(document.documentElement.lang || 'en', { style: 'currency', currency: 'USD' });
toggle.setAttribute('role', 'button');
toggle.setAttribute('aria-expanded', 'false');
const setOpen = (open, returnFocus = true) => {
panel.hidden = !open;
toggle.setAttribute('aria-expanded', String(open));
if (open) {
panel.focus();
} else if (returnFocus) {
toggle.focus();
}
};
const update = () => {
const items = [...list.querySelectorAll('[data-mini-cart-item]')];
const count = items.reduce((sum, item) => sum + Number(item.dataset.quantity), 0);
const subtotal = items.reduce((sum, item) => sum + Number(item.dataset.price) * Number(item.dataset.quantity), 0);
cart.querySelector('[data-mini-cart-count]').textContent = count;
cart.querySelector('[data-mini-cart-subtotal]').textContent = format.format(subtotal);
cart.querySelector('[data-mini-cart-empty]').hidden = count > 0;
cart.querySelector('.mini-cart__footer').hidden = count === 0;
list.hidden = count === 0;
};
toggle.addEventListener('click', (event) => {
event.preventDefault();
setOpen(panel.hidden);
});
toggle.addEventListener('keydown', (event) => {
if (event.key === ' ') {
event.preventDefault();
setOpen(panel.hidden);
}
});
cart.querySelector('[data-mini-cart-close]').addEventListener('click', () => setOpen(false));
cart.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && !panel.hidden) {
setOpen(false);
}
});
document.addEventListener('click', (event) => {
if (!panel.hidden && !cart.contains(event.target)) {
setOpen(false, false);
}
});
list.addEventListener('click', (event) => {
const button = event.target.closest('[data-mini-cart-remove]');
if (!button) {
return;
}
const item = button.closest('[data-mini-cart-item]');
const name = item.querySelector('.mini-cart__name').textContent;
const next = item.nextElementSibling || item.previousElementSibling;
item.remove();
update();
live.textContent = `${name} removed. Subtotal: ${cart.querySelector('[data-mini-cart-subtotal]').textContent}.`;
cart.dispatchEvent(new CustomEvent('cart:remove', { bubbles: true, detail: { name } }));
(next?.querySelector('[data-mini-cart-remove]') || panel).focus();
});
update();
});
})();