@use 'sprucecss/scss/spruce' as *;
.gift-card {
align-items: start;
display: grid;
gap: spacer-clamp('m', 'l');
@include breakpoint('md') {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}
&__preview {
@include breakpoint('md') {
inset-block-start: spacer('m');
position: sticky;
}
}
&__card {
aspect-ratio: 1.586;
background-color: color('primary');
background-image:
radial-gradient(circle at 100% 0%, #{color-alpha('secondary', 0.85)}, transparent 55%),
radial-gradient(circle at 0% 100%, hsl(0deg 0% 100% / 18%), transparent 50%);
border-radius: radius('lg');
box-shadow: shadow('lg');
color: hsl(0deg 0% 100%);
display: grid;
grid-template-rows: auto 1fr auto auto;
padding: spacer-clamp('m', 'l');
}
&__brand {
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
}
&__amount {
align-self: center;
font-family: config('font-family-heading', $typography);
font-size: clamp(2.25rem, 6vw, 3.5rem);
font-variant-numeric: tabular-nums;
font-weight: 800;
}
&__to {
font-weight: 600;
}
&__message {
font-size: config('font-size-sm', $typography);
opacity: 0.85;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__form {
@include layout-stack('m');
}
&__title {
font-size: font-size('h4');
}
&__amounts {
border: 0;
margin: 0;
padding: 0;
@include layout-stack('s');
legend {
font-size: config('font-size-base', $typography);
}
}
&__options {
display: grid;
gap: spacer('xs');
grid-template-columns: repeat(4, minmax(0, 1fr));
}
&__option {
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: 700;
justify-content: center;
min-block-size: 3rem;
}
input:checked + span {
background-color: color-alpha('primary', 0.1);
border-color: color('primary');
box-shadow: 0 0 0 1px color('primary');
color: color('primary');
}
input:focus-visible + span {
outline: 2px solid color('focus-ring');
outline-offset: 2px;
}
}
&__custom {
@include layout-stack('xxs');
&[hidden] {
display: none;
}
}
&__currency {
align-items: center;
display: flex;
position: relative;
span {
color: color('muted');
inset-inline-start: spacer('s');
pointer-events: none;
position: absolute;
}
.form-control {
padding-inline-start: spacer('l');
}
}
&__row {
@include layout-switcher(28rem, 'm');
}
}
<section class="gift-card" aria-labelledby="gift-card-title" data-gift-card>
<div class="gift-card__preview" aria-hidden="true">
<div class="gift-card__card">
<span class="gift-card__brand">Spruce Store</span>
<span class="gift-card__amount" data-gift-card-amount>$50</span>
<span class="gift-card__to" data-gift-card-to>For someone special</span>
<span class="gift-card__message" data-gift-card-message>Enjoy!</span>
</div>
</div>
<form class="gift-card__form" action="#" method="post" data-gift-card-form>
<h2 class="gift-card__title" id="gift-card-title">Send a gift card</h2>
<fieldset class="gift-card__amounts">
<legend class="form-label">Amount</legend>
<div class="gift-card__options">
<label class="gift-card__option"><input type="radio" name="amount" value="25"><span>$25</span></label>
<label class="gift-card__option"><input type="radio" name="amount" value="50" checked><span>$50</span></label>
<label class="gift-card__option"><input type="radio" name="amount" value="100"><span>$100</span></label>
<label class="gift-card__option"><input type="radio" name="amount" value="custom"><span>Other</span></label>
</div>
<div class="gift-card__custom" data-gift-card-custom>
<label class="form-label" for="gift-card-custom">Custom amount <span class="form-description">($10–$500)</span></label>
<div class="gift-card__currency">
<span aria-hidden="true">$</span>
<input class="form-control" id="gift-card-custom" name="custom" type="number" inputmode="numeric" min="10" max="500" step="1" placeholder="75">
</div>
</div>
</fieldset>
<div class="gift-card__row">
<div class="form-group">
<label class="form-label form-label--required" for="gift-card-name">Recipient name</label>
<input class="form-control" id="gift-card-name" name="name" type="text" autocomplete="off" required>
</div>
<div class="form-group">
<label class="form-label form-label--required" for="gift-card-email">Recipient email</label>
<input class="form-control" id="gift-card-email" name="email" type="email" autocomplete="off" required>
</div>
</div>
<div class="form-group">
<label class="form-label" for="gift-card-message">Message <span class="form-description" data-gift-card-counter>(120 characters left)</span></label>
<textarea class="form-control" id="gift-card-message" name="message" rows="3" maxlength="120" placeholder="Enjoy!"></textarea>
</div>
<div class="form-group">
<label class="form-label" for="gift-card-date">Delivery date</label>
<input class="form-control" id="gift-card-date" name="date" type="date">
<p class="form-description">Leave it empty to send it right away.</p>
</div>
<button class="btn btn--primary btn--lg" type="submit">Add to cart</button>
</form>
</section>
(() => {
document.querySelectorAll('[data-gift-card]').forEach((section) => {
const form = section.querySelector('[data-gift-card-form]');
const custom = section.querySelector('[data-gift-card-custom]');
const counter = section.querySelector('[data-gift-card-counter]');
const preview = {
amount: section.querySelector('[data-gift-card-amount]'),
to: section.querySelector('[data-gift-card-to]'),
message: section.querySelector('[data-gift-card-message]')
};
const { amount, custom: customInput, name, message } = form.elements;
const update = () => {
const isCustom = amount.value === 'custom';
custom.hidden = !isCustom;
customInput.required = isCustom;
preview.amount.textContent = `$${isCustom ? (customInput.value || '—') : amount.value}`;
preview.to.textContent = name.value.trim() ? `For ${name.value.trim()}` : 'For someone special';
preview.message.textContent = message.value.trim() || 'Enjoy!';
counter.textContent = `(${message.maxLength - message.value.length} characters left)`;
};
form.addEventListener('input', update);
form.addEventListener('change', (event) => {
update();
if (event.target.name === 'amount' && event.target.value === 'custom') customInput.focus();
});
update();
});
})();