@use 'sprucecss/scss/spruce' as *;
.cart-drawer {
--drawer-inline-size: 26rem;
&__count {
color: color('muted');
font-weight: 400;
}
&__shipping {
background-color: color('surface-muted');
border-radius: radius('md');
padding: spacer('s');
@include layout-stack('xs');
}
&__shipping-text {
font-size: config('font-size-sm', $typography);
}
&__progress {
appearance: none;
background-color: color('border');
block-size: 0.5rem;
border: 0;
border-radius: radius('pill');
display: block;
inline-size: 100%;
overflow: hidden;
&::-webkit-progress-bar {
background-color: color('border');
}
&::-webkit-progress-value {
background-color: color('success', 'alert');
border-radius: radius('pill');
}
&::-moz-progress-bar {
background-color: color('success', 'alert');
border-radius: radius('pill');
}
}
&__items {
list-style: none;
padding: 0;
}
&__item {
align-items: start;
border-block-end: config('border-width-base', $display) solid color('border');
display: grid;
gap: spacer('s');
grid-template-columns: 5rem minmax(0, 1fr) auto;
padding-block: spacer('s');
}
&__thumb {
aspect-ratio: 1;
background-color: color('surface-muted');
border-radius: radius('md');
inline-size: 100%;
object-fit: cover;
}
&__info {
@include layout-stack('xxs');
}
&__name {
color: color('heading');
display: block;
font-weight: 700;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
&__meta {
color: color('muted');
font-size: config('font-size-sm', $typography);
}
&__quantity {
align-items: center;
border: config('border-width-base', $display) solid color('border');
border-radius: radius('pill');
display: inline-flex;
}
&__step {
align-items: center;
background: none;
block-size: 2rem;
border: 0;
border-radius: 50%;
color: color('heading');
cursor: pointer;
display: flex;
font-size: 1.1rem;
inline-size: 2rem;
justify-content: center;
&:hover {
background-color: color('surface-muted');
}
&:focus-visible {
outline: 2px solid color('focus-ring');
outline-offset: 1px;
}
}
&__value {
font-variant-numeric: tabular-nums;
font-weight: 700;
min-inline-size: 2ch;
text-align: center;
}
&__price {
color: color('heading');
font-variant-numeric: tabular-nums;
font-weight: 700;
margin-block: 0;
}
&__empty {
color: color('muted');
padding-block: spacer('l');
text-align: center;
}
&__footer {
border-block-start: config('border-width-base', $display) solid color('border');
display: flex;
flex-direction: column;
gap: spacer('xs');
padding: spacer('m');
}
&__subtotal {
color: color('heading');
display: flex;
font-size: font-size('h5');
justify-content: space-between;
margin-block: 0;
}
&__note {
color: color('muted');
font-size: config('font-size-sm', $typography);
margin-block: 0 spacer('xs');
}
}
<button class="btn btn--primary" type="button" data-drawer-open="cart-drawer" aria-haspopup="dialog">Open the cart</button>
<dialog class="drawer cart-drawer" id="cart-drawer" aria-labelledby="cart-drawer-title" data-cart-drawer data-free-shipping="150">
<div class="drawer__panel">
<header class="drawer__header">
<h2 class="drawer__title" id="cart-drawer-title">Your cart <span class="cart-drawer__count">(<span data-cart-drawer-count>2</span>)</span></h2>
<button class="btn btn--ghost-secondary btn--icon btn--sm" type="button" data-drawer-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>
<div class="drawer__body cart-drawer__body">
<div class="cart-drawer__shipping">
<p class="cart-drawer__shipping-text" data-cart-drawer-shipping>Add <strong>$19.00</strong> more for free shipping.</p>
<progress class="cart-drawer__progress" max="150" value="131" data-cart-drawer-progress>131 of 150</progress>
</div>
<ul class="cart-drawer__items" role="list">
<li class="cart-drawer__item" data-cart-drawer-item data-price="89">
<img class="cart-drawer__thumb" src="https://images.unsplash.com/photo-1553062407-98eeb64c6a62?auto=format&fit=crop&w=160&h=160&q=60" alt="" width="80" height="80" loading="lazy">
<div class="cart-drawer__info">
<a class="cart-drawer__name" href="#">Everyday Backpack</a>
<p class="cart-drawer__meta">Charcoal · 22 L</p>
<div class="cart-drawer__quantity" role="group" aria-label="Quantity of Everyday Backpack">
<button class="cart-drawer__step" type="button" data-cart-drawer-step="-1"><span aria-hidden="true">−</span><span class="sr-only">Decrease</span></button>
<output class="cart-drawer__value" data-cart-drawer-quantity>1</output>
<button class="cart-drawer__step" type="button" data-cart-drawer-step="1"><span aria-hidden="true">+</span><span class="sr-only">Increase</span></button>
</div>
</div>
<p class="cart-drawer__price" data-cart-drawer-line>$89.00</p>
</li>
<li class="cart-drawer__item" data-cart-drawer-item data-price="42">
<img class="cart-drawer__thumb" src="https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?auto=format&fit=crop&w=160&h=160&q=60" alt="" width="80" height="80" loading="lazy">
<div class="cart-drawer__info">
<a class="cart-drawer__name" href="#">Ceramic Pour-Over</a>
<p class="cart-drawer__meta">White</p>
<div class="cart-drawer__quantity" role="group" aria-label="Quantity of Ceramic Pour-Over">
<button class="cart-drawer__step" type="button" data-cart-drawer-step="-1"><span aria-hidden="true">−</span><span class="sr-only">Decrease</span></button>
<output class="cart-drawer__value" data-cart-drawer-quantity>1</output>
<button class="cart-drawer__step" type="button" data-cart-drawer-step="1"><span aria-hidden="true">+</span><span class="sr-only">Increase</span></button>
</div>
</div>
<p class="cart-drawer__price" data-cart-drawer-line>$42.00</p>
</li>
</ul>
<p class="cart-drawer__empty" data-cart-drawer-empty hidden>Your cart is empty.</p>
</div>
<footer class="cart-drawer__footer">
<p class="cart-drawer__subtotal"><span>Subtotal</span> <strong data-cart-drawer-subtotal>$131.00</strong></p>
<p class="cart-drawer__note">Taxes calculated at checkout.</p>
<a class="btn btn--primary btn--block" href="#">Checkout</a>
<button class="btn btn--ghost-primary btn--block" type="button" data-drawer-close>Continue shopping</button>
</footer>
<p class="sr-only" aria-live="polite" data-cart-drawer-live></p>
</div>
</dialog>
(() => {
const money = (value) => `$${value.toFixed(2)}`;
document.querySelectorAll('[data-cart-drawer]').forEach((drawer) => {
const freeShipping = Number(drawer.dataset.freeShipping);
const subtotalOutput = drawer.querySelector('[data-cart-drawer-subtotal]');
const countOutput = drawer.querySelector('[data-cart-drawer-count]');
const shippingText = drawer.querySelector('[data-cart-drawer-shipping]');
const progress = drawer.querySelector('[data-cart-drawer-progress]');
const empty = drawer.querySelector('[data-cart-drawer-empty]');
const live = drawer.querySelector('[data-cart-drawer-live]');
const update = () => {
const items = [...drawer.querySelectorAll('[data-cart-drawer-item]')];
let subtotal = 0;
let count = 0;
items.forEach((item) => {
const quantity = Number(item.querySelector('[data-cart-drawer-quantity]').value);
const line = Number(item.dataset.price) * quantity;
item.querySelector('[data-cart-drawer-line]').textContent = money(line);
subtotal += line;
count += quantity;
});
subtotalOutput.textContent = money(subtotal);
countOutput.textContent = count;
empty.hidden = items.length > 0;
progress.value = Math.min(subtotal, freeShipping);
progress.textContent = `${Math.min(subtotal, freeShipping)} of ${freeShipping}`;
shippingText.innerHTML = subtotal >= freeShipping
? 'You get <strong>free shipping</strong>.'
: `Add <strong>${money(freeShipping - subtotal)}</strong> more for free shipping.`;
return subtotal;
};
drawer.addEventListener('click', (event) => {
const step = event.target.closest('[data-cart-drawer-step]');
if (!step) {
return;
}
const item = step.closest('[data-cart-drawer-item]');
const output = item.querySelector('[data-cart-drawer-quantity]');
const name = item.querySelector('.cart-drawer__name').textContent;
const quantity = Number(output.value) + Number(step.dataset.cartDrawerStep);
if (quantity < 1) {
const next = item.nextElementSibling || item.previousElementSibling;
item.remove();
(next?.querySelector('[data-cart-drawer-step]') || drawer.querySelector('[data-drawer-close]')).focus();
update();
live.textContent = `${name} removed from the cart.`;
return;
}
output.value = quantity;
live.textContent = `${name}: quantity ${quantity}. Subtotal ${money(update())}.`;
});
update();
});
})();