@use 'sprucecss/scss/spruce' as *;
.stock-indicator {
--stock-in: #{color('success', 'alert')};
--stock-low: #{color('warning', 'alert')};
--stock-out: #{color('danger', 'alert')};
@include layout-stack('m');
&__sizes {
border: 0;
display: flex;
flex-wrap: wrap;
gap: spacer('xs');
margin: 0;
padding: 0;
}
&__legend {
color: color('heading');
font-size: config('font-size-base', $typography);
font-weight: 600;
margin-block-end: spacer('xs');
padding: 0;
}
&__size {
cursor: pointer;
position: relative;
input {
inset: 0;
opacity: 0;
position: absolute;
}
span {
align-items: center;
border: config('border-width-base', $display) solid color('border', 'form');
border-radius: radius('md');
color: color('heading');
display: flex;
font-weight: 600;
justify-content: center;
min-block-size: 2.75rem;
min-inline-size: 3.25rem;
padding-inline: spacer('s');
}
input:checked + span {
background-color: color('heading');
border-color: color('heading');
color: color('background');
}
input:focus-visible + span {
outline: 2px solid color('focus-ring');
outline-offset: 2px;
}
input[data-stock='0'] + span {
background-image: linear-gradient(to top right, transparent calc(50% - 1px), color('border-strong') 50%, transparent calc(50% + 1px));
color: color('muted');
}
input[data-stock='0']:checked + span {
background-color: color('surface-muted');
border-color: color('heading');
color: color('heading');
}
}
&__status {
--stock-color: var(--stock-in);
align-items: center;
display: flex;
gap: spacer('s');
p {
margin: 0;
}
strong {
color: var(--stock-color);
margin-inline-end: spacer('xxs');
}
span {
color: color('muted');
}
&--low {
--stock-color: var(--stock-low);
}
&--out {
--stock-color: var(--stock-out);
}
}
&__level {
display: flex;
flex-shrink: 0;
gap: 3px;
span {
background-color: var(--stock-color);
block-size: 0.75rem;
border-radius: 2px;
inline-size: 0.4rem;
}
}
&__status--low &__level span:nth-child(n + 2),
&__status--out &__level span {
background-color: color('border');
}
&__notify {
background-color: color('surface-muted');
border-radius: radius('md');
padding: spacer('s');
@include layout-stack('xs');
&[hidden] {
display: none;
}
}
&__notify-row {
display: flex;
flex-wrap: wrap;
gap: spacer('xs');
.form-control {
flex: 1 1 12rem;
}
}
&__stores {
border-block-start: config('border-width-base', $display) solid color('border');
font-size: config('font-size-sm', $typography);
list-style: none;
padding: spacer('s') 0 0;
@include layout-stack('xs');
li {
align-items: baseline;
display: flex;
gap: spacer('xs');
}
strong {
color: color('heading');
display: block;
}
}
&__dot {
block-size: 0.55rem;
border-radius: radius('pill');
flex-shrink: 0;
inline-size: 0.55rem;
&--in {
background-color: var(--stock-in);
}
&--low {
background-color: var(--stock-low);
}
&--out {
background-color: var(--stock-out);
}
}
}
<section class="stock-indicator" aria-label="Availability" data-stock-indicator>
<fieldset class="stock-indicator__sizes">
<legend class="stock-indicator__legend">Size</legend>
<label class="stock-indicator__size"><input type="radio" name="stock-size" value="S" data-stock="14"><span>S</span></label>
<label class="stock-indicator__size"><input type="radio" name="stock-size" value="M" data-stock="3" checked><span>M</span></label>
<label class="stock-indicator__size"><input type="radio" name="stock-size" value="L" data-stock="0"><span>L</span></label>
<label class="stock-indicator__size"><input type="radio" name="stock-size" value="XL" data-stock="8"><span>XL</span></label>
</fieldset>
<div class="stock-indicator__status stock-indicator__status--low" aria-live="polite" data-stock-status>
<span class="stock-indicator__level" aria-hidden="true"><span></span><span></span><span></span></span>
<p><strong data-stock-text>Only 3 left</strong> <span data-stock-detail>Order soon. Ships tomorrow.</span></p>
</div>
<form class="stock-indicator__notify" action="#" method="post" hidden data-stock-notify>
<label class="form-label" for="stock-notify-email">Get an email when it’s back</label>
<div class="stock-indicator__notify-row">
<input class="form-control" id="stock-notify-email" name="email" type="email" autocomplete="email" placeholder="you@example.com" required>
<button class="btn btn--secondary" type="submit">Notify me</button>
</div>
</form>
<ul class="stock-indicator__stores" role="list" aria-label="Pick up in a store">
<li><span class="stock-indicator__dot stock-indicator__dot--in" aria-hidden="true"></span><span><strong>Portland, Pearl District</strong> Available today</span></li>
<li><span class="stock-indicator__dot stock-indicator__dot--low" aria-hidden="true"></span><span><strong>Seattle, Capitol Hill</strong> Limited stock</span></li>
<li><span class="stock-indicator__dot stock-indicator__dot--out" aria-hidden="true"></span><span><strong>San Francisco, Mission</strong> Not available</span></li>
</ul>
</section>
(() => {
const low = 5;
document.querySelectorAll('[data-stock-indicator]').forEach((indicator) => {
const status = indicator.querySelector('[data-stock-status]');
const text = indicator.querySelector('[data-stock-text]');
const detail = indicator.querySelector('[data-stock-detail]');
const notify = indicator.querySelector('[data-stock-notify]');
const update = (input) => {
const stock = Number(input.dataset.stock);
const state = stock === 0 ? 'out' : stock <= low ? 'low' : 'in';
status.className = `stock-indicator__status stock-indicator__status--${state}`;
text.textContent = { in: 'In stock', low: `Only ${stock} left`, out: 'Out of stock' }[state];
detail.textContent = {
in: `Size ${input.value} ships tomorrow.`,
low: `Size ${input.value}: order soon. Ships tomorrow.`,
out: `Size ${input.value} is sold out.`
}[state];
notify.hidden = state !== 'out';
};
indicator.addEventListener('change', (event) => {
if (event.target.matches('[data-stock]')) update(event.target);
});
notify.addEventListener('submit', (event) => {
event.preventDefault();
if (!notify.reportValidity()) return;
detail.textContent = `We’ll email ${notify.elements.email.value} when it’s back.`;
notify.hidden = true;
indicator.querySelector('[data-stock]:checked').focus();
});
update(indicator.querySelector('[data-stock]:checked'));
});
})();