@use 'sprucecss/scss/spruce' as *;
.wishlist {
@include layout-stack('m');
&__header {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: spacer('s');
justify-content: space-between;
}
&__title {
font-size: font-size('h4');
}
&__count {
color: color('muted');
font-family: config('font-family-base', $typography);
font-size: config('font-size-base', $typography);
font-weight: 400;
}
&__items {
display: grid;
gap: spacer('m');
grid-template-columns: repeat(auto-fill, minmax(min(13rem, 100%), 1fr));
list-style: none;
padding: 0;
> li {
margin-block: 0;
padding-block: 0;
}
}
&__item {
display: flex;
}
&__card {
background-color: color('surface');
border: config('border-width-base', $display) solid color('border');
border-radius: radius('lg');
display: flex;
flex-direction: column;
inline-size: 100%;
overflow: hidden;
}
&__image {
aspect-ratio: 1;
background-color: color('surface-muted');
inline-size: 100%;
object-fit: cover;
}
&__body {
flex-grow: 1;
padding: spacer('s') spacer('s') 0;
@include layout-stack('xxs');
}
&__name {
font-family: inherit;
font-size: config('font-size-base', $typography);
font-weight: 600;
a {
color: color('heading');
text-decoration: none;
&:hover {
color: color('primary');
text-decoration: underline;
}
}
}
&__variant {
color: color('muted');
font-size: config('font-size-sm', $typography);
}
&__price {
color: color('heading');
font-weight: 700;
del {
color: color('muted');
font-size: config('font-size-sm', $typography);
font-weight: 400;
}
}
&__stock {
align-items: center;
display: flex;
font-size: config('font-size-sm', $typography);
gap: spacer('xxs');
&::before {
background-color: currentcolor;
block-size: 0.5rem;
border-radius: radius('pill');
content: '';
inline-size: 0.5rem;
}
&--in {
color: color('success', 'alert');
}
&--low {
color: color('warning', 'alert');
}
&--out {
color: color('muted');
}
}
&__actions {
display: flex;
gap: spacer('xs');
padding: spacer('s');
.btn:first-child {
flex-grow: 1;
}
}
&__empty {
border: config('border-width-base', $display) dashed color('border');
border-radius: radius('lg');
padding: spacer('l') spacer('m');
text-align: center;
@include layout-stack('s');
&[hidden] {
display: none;
}
}
}
<section class="wishlist" aria-labelledby="wishlist-title" data-wishlist>
<header class="wishlist__header">
<h2 class="wishlist__title" id="wishlist-title">Wishlist <span class="wishlist__count" data-wishlist-count>(4 items)</span></h2>
<button class="btn btn--secondary btn--sm" type="button" data-wishlist-share>Share list</button>
</header>
<ul class="wishlist__items" role="list" data-wishlist-items>
<li class="wishlist__item" data-wishlist-item>
<article class="wishlist__card" aria-labelledby="wishlist-item-1">
<img class="wishlist__image" src="https://images.unsplash.com/photo-1596755094514-f87e34085b2c?auto=format&fit=crop&w=400&q=70" alt="" width="400" height="400" loading="lazy">
<div class="wishlist__body">
<h3 class="wishlist__name" id="wishlist-item-1"><a href="#">Linen Overshirt</a></h3>
<p class="wishlist__variant">Sand · M</p>
<p class="wishlist__price"><span class="sr-only">Sale price: </span>$79 <del><span class="sr-only">Original price: </span>$98</del></p>
<p class="wishlist__stock wishlist__stock--in">In stock</p>
</div>
<div class="wishlist__actions">
<button class="btn btn--primary btn--sm" type="button" data-wishlist-move>Move to cart<span class="sr-only">: Linen Overshirt</span></button>
<button class="btn btn--ghost-danger btn--icon btn--sm" type="button" data-wishlist-remove><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"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg><span class="sr-only">Remove Linen Overshirt from the wishlist</span></button>
</div>
</article>
</li>
<li class="wishlist__item" data-wishlist-item>
<article class="wishlist__card" aria-labelledby="wishlist-item-2">
<img class="wishlist__image" src="https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?auto=format&fit=crop&w=400&q=70" alt="" width="400" height="400" loading="lazy">
<div class="wishlist__body">
<h3 class="wishlist__name" id="wishlist-item-2"><a href="#">Ceramic Pour-Over</a></h3>
<p class="wishlist__variant">Matte white</p>
<p class="wishlist__price">$42</p>
<p class="wishlist__stock wishlist__stock--low">Only 2 left</p>
</div>
<div class="wishlist__actions">
<button class="btn btn--primary btn--sm" type="button" data-wishlist-move>Move to cart<span class="sr-only">: Ceramic Pour-Over</span></button>
<button class="btn btn--ghost-danger btn--icon btn--sm" type="button" data-wishlist-remove><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"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg><span class="sr-only">Remove Ceramic Pour-Over from the wishlist</span></button>
</div>
</article>
</li>
<li class="wishlist__item" data-wishlist-item>
<article class="wishlist__card" aria-labelledby="wishlist-item-3">
<img class="wishlist__image" src="https://images.unsplash.com/photo-1505740420928-5e560c06d30e?auto=format&fit=crop&w=400&q=70" alt="" width="400" height="400" loading="lazy">
<div class="wishlist__body">
<h3 class="wishlist__name" id="wishlist-item-3"><a href="#">Wireless Headphones</a></h3>
<p class="wishlist__variant">Graphite</p>
<p class="wishlist__price">$199</p>
<p class="wishlist__stock wishlist__stock--out">Out of stock</p>
</div>
<div class="wishlist__actions">
<button class="btn btn--outline-primary btn--sm" type="button" aria-pressed="false" data-wishlist-notify>Notify me<span class="sr-only"> when Wireless Headphones is back</span></button>
<button class="btn btn--ghost-danger btn--icon btn--sm" type="button" data-wishlist-remove><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"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg><span class="sr-only">Remove Wireless Headphones from the wishlist</span></button>
</div>
</article>
</li>
<li class="wishlist__item" data-wishlist-item>
<article class="wishlist__card" aria-labelledby="wishlist-item-4">
<img class="wishlist__image" src="https://images.unsplash.com/photo-1547949003-9792a18a2601?auto=format&fit=crop&w=400&q=70" alt="" width="400" height="400" loading="lazy">
<div class="wishlist__body">
<h3 class="wishlist__name" id="wishlist-item-4"><a href="#">Leather Weekender</a></h3>
<p class="wishlist__variant">Cognac</p>
<p class="wishlist__price">$249</p>
<p class="wishlist__stock wishlist__stock--in">In stock</p>
</div>
<div class="wishlist__actions">
<button class="btn btn--primary btn--sm" type="button" data-wishlist-move>Move to cart<span class="sr-only">: Leather Weekender</span></button>
<button class="btn btn--ghost-danger btn--icon btn--sm" type="button" data-wishlist-remove><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"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg><span class="sr-only">Remove Leather Weekender from the wishlist</span></button>
</div>
</article>
</li>
</ul>
<div class="wishlist__empty" hidden data-wishlist-empty>
<p>Your wishlist is empty. Tap the heart on any product to save it for later.</p>
<a class="btn btn--primary btn--sm" href="#">Continue shopping</a>
</div>
<p class="sr-only" aria-live="polite" data-wishlist-status></p>
</section>
(() => {
document.querySelectorAll('[data-wishlist]').forEach((wishlist) => {
const list = wishlist.querySelector('[data-wishlist-items]');
const count = wishlist.querySelector('[data-wishlist-count]');
const empty = wishlist.querySelector('[data-wishlist-empty]');
const status = wishlist.querySelector('[data-wishlist-status]');
const share = wishlist.querySelector('[data-wishlist-share]');
const update = () => {
const total = list.children.length;
count.textContent = `(${total} ${total === 1 ? 'item' : 'items'})`;
empty.hidden = total > 0;
list.hidden = total === 0;
share.hidden = total === 0;
};
const removeItem = (item, message) => {
const next = item.nextElementSibling || item.previousElementSibling;
item.remove();
update();
status.textContent = message;
(next?.querySelector('a') || empty.querySelector('a')).focus();
};
list.addEventListener('click', (event) => {
const item = event.target.closest('[data-wishlist-item]');
if (!item) return;
const name = item.querySelector('.wishlist__name').textContent.trim();
const notify = event.target.closest('[data-wishlist-notify]');
if (event.target.closest('[data-wishlist-move]')) {
removeItem(item, `${name} moved to the cart.`);
} else if (event.target.closest('[data-wishlist-remove]')) {
removeItem(item, `${name} removed from the wishlist.`);
} else if (notify) {
const isOn = notify.getAttribute('aria-pressed') !== 'true';
notify.setAttribute('aria-pressed', String(isOn));
notify.firstChild.textContent = isOn ? 'We’ll email you' : 'Notify me';
status.textContent = isOn ? `We’ll email you when ${name} is back in stock.` : `Back-in-stock email for ${name} turned off.`;
}
});
share.addEventListener('click', async () => {
const data = { title: 'My wishlist', url: window.location.href };
if (navigator.share) {
try {
await navigator.share(data);
} catch (error) {}
} else if (navigator.clipboard) {
await navigator.clipboard.writeText(data.url);
status.textContent = 'The wishlist link is copied to the clipboard.';
}
});
update();
});
})();