@use 'sprucecss/scss/spruce' as *;
.quick-view-trigger {
background-color: color('surface');
border: config('border-width-base', $display) solid color('border');
border-radius: radius('lg');
inline-size: 15rem;
padding: spacer('s');
@include layout-stack('xs');
img {
aspect-ratio: 1;
border-radius: radius('md');
inline-size: 100%;
object-fit: cover;
}
&__title {
font-family: inherit;
font-size: config('font-size-base', $typography);
}
}
.quick-view {
background-color: color('surface');
border: config('border-width-base', $display) solid color('border');
border-radius: radius('lg');
box-shadow: shadow('xl');
inline-size: min(56rem, calc(100vi - #{spacer('m')} * 2));
max-block-size: calc(100dvb - #{spacer('m')} * 2);
max-inline-size: none;
overflow-y: auto;
padding: 0;
&::backdrop {
background-color: color('overlay');
}
@include motion-safe {
&[open] {
animation: quick-view-in 0.2s ease-out;
}
}
&__inner {
display: grid;
position: relative;
@include breakpoint('sm') {
grid-template-columns: 1fr 1fr;
}
}
&__close {
background-color: color-alpha('surface', 0.85) !important;
inset-block-start: spacer('xs');
inset-inline-end: spacer('xs');
position: absolute;
z-index: 1;
}
&__media {
aspect-ratio: 4 / 3;
background-color: color('surface-muted');
block-size: 100%;
inline-size: 100%;
object-fit: cover;
@include breakpoint('sm') {
aspect-ratio: auto;
}
}
&__body {
padding: spacer-clamp('m', 'l');
@include layout-stack('s');
}
&__heading {
@include layout-stack('xxs');
}
&__title {
font-size: font-size('h3');
margin: 0;
padding-inline-end: spacer('l');
}
&__rating {
color: color('muted');
font-size: config('font-size-sm', $typography);
span[aria-hidden] {
color: color('warning', 'alert');
letter-spacing: 0.1em;
}
}
&__price {
color: color('heading');
font-size: font-size('h4');
font-weight: 700;
}
&__option {
border: 0;
margin-inline: 0;
padding: 0;
}
&__legend {
color: color('heading');
font-size: config('font-size-base', $typography);
font-weight: 700;
margin-block-end: spacer('xs');
padding: 0;
}
&__swatches {
display: flex;
gap: spacer('xs');
}
&__swatch {
aspect-ratio: 1;
background-color: var(--swatch);
border: 2px solid color('surface');
border-radius: radius('pill');
box-shadow: 0 0 0 1px color('border');
cursor: pointer;
inline-size: 2rem;
position: relative;
input {
appearance: none;
border-radius: inherit;
cursor: pointer;
inset: 0;
margin: 0;
opacity: 0;
position: absolute;
}
&:has(:checked) {
box-shadow: 0 0 0 2px color('heading');
}
&:has(:focus-visible) {
outline: 2px solid color('focus-ring');
outline-offset: 3px;
}
}
&__buy {
display: grid;
gap: spacer('xs');
grid-template-columns: 5rem 1fr;
}
&__status {
color: color('heading');
font-size: config('font-size-sm', $typography);
&:not(:empty) {
background-color: color-alpha('success', 0.12, 'alert');
border-radius: radius('md');
padding: spacer('xs') spacer('s');
}
}
&__link {
display: inline-block;
font-size: config('font-size-sm', $typography);
font-weight: 600;
}
}
@keyframes quick-view-in {
from {
opacity: 0;
scale: 0.96;
}
}
<article class="quick-view-trigger">
<img src="https://images.unsplash.com/photo-1505740420928-5e560c06d30e?auto=format&fit=crop&w=480&h=480&q=60" alt="" width="240" height="240" loading="lazy">
<h3 class="quick-view-trigger__title">Wireless Headphones</h3>
<a class="btn btn--outline-primary btn--sm" href="#" data-quick-view-open="quick-view-headphones">Quick view<span class="sr-only">: Wireless Headphones</span></a>
</article>
<dialog class="quick-view" id="quick-view-headphones" aria-labelledby="quick-view-title">
<div class="quick-view__inner">
<button class="btn btn--ghost-secondary btn--icon btn--sm quick-view__close" type="button" data-quick-view-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</span>
</button>
<img class="quick-view__media" src="https://images.unsplash.com/photo-1505740420928-5e560c06d30e?auto=format&fit=crop&w=800&h=800&q=70" alt="Wireless Headphones in black, side view." width="800" height="800" loading="lazy">
<form class="quick-view__body" action="#" method="post" data-quick-view-form>
<div class="quick-view__heading">
<h2 class="quick-view__title" id="quick-view-title">Wireless Headphones</h2>
<p class="quick-view__rating"><span aria-hidden="true">★★★★★</span> <span class="sr-only">Rated 4.8 out of 5,</span> 412 reviews</p>
</div>
<p class="quick-view__price">$199.00</p>
<p class="quick-view__text">Active noise cancelling, 40 hours of battery life and a foldable design that fits in every bag.</p>
<fieldset class="quick-view__option">
<legend class="quick-view__legend">Color</legend>
<div class="quick-view__swatches">
<label class="quick-view__swatch" style="--swatch: hsl(0deg 0% 12%);"><input type="radio" name="color" value="black" checked><span class="sr-only">Black</span></label>
<label class="quick-view__swatch" style="--swatch: hsl(35deg 25% 85%);"><input type="radio" name="color" value="sand"><span class="sr-only">Sand</span></label>
<label class="quick-view__swatch" style="--swatch: hsl(215deg 35% 35%);"><input type="radio" name="color" value="navy"><span class="sr-only">Navy</span></label>
</div>
</fieldset>
<div class="quick-view__buy">
<label class="sr-only" for="quick-view-quantity">Quantity</label>
<input class="form-control" id="quick-view-quantity" name="quantity" type="number" min="1" value="1" inputmode="numeric">
<button class="btn btn--primary" type="submit" data-quick-view-add>Add to cart</button>
</div>
<p class="quick-view__status" role="status" data-quick-view-status></p>
<a class="quick-view__link" href="#">View full details</a>
</form>
</div>
</dialog>
(() => {
document.querySelectorAll('[data-quick-view-open]').forEach((opener) => {
const dialog = document.getElementById(opener.dataset.quickViewOpen);
if (!dialog) {
return;
}
const form = dialog.querySelector('[data-quick-view-form]');
const status = dialog.querySelector('[data-quick-view-status]');
opener.setAttribute('role', 'button');
opener.setAttribute('aria-haspopup', 'dialog');
const open = (event) => {
event.preventDefault();
status.textContent = '';
dialog.showModal();
};
opener.addEventListener('click', open);
opener.addEventListener('keydown', (event) => {
if (event.key === ' ') {
open(event);
}
});
dialog.addEventListener('click', (event) => {
if (event.target === dialog || event.target.closest('[data-quick-view-close]')) {
dialog.close();
}
});
form.addEventListener('submit', (event) => {
event.preventDefault();
form.dispatchEvent(new CustomEvent('cart:add', { bubbles: true, detail: { data: new FormData(form) } }));
status.innerHTML = '';
status.append(`${form.elements.quantity.value} × ${dialog.querySelector('.quick-view__title').textContent} added to your cart. `);
const link = document.createElement('a');
link.href = '#';
link.textContent = 'View cart';
status.append(link);
});
});
})();