Gift Card

Send a gift card

Amount

Leave it empty to send it right away.

@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;
    }
  }

  // A credit card ratio, with the brand colors.
  &__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';

      // The custom field is only shown (and required) for "Other".
      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();
  });
})();

Gift Card

A gift card purchase form with preset or custom amounts, the recipient, a message and a delivery date, next to a live preview of the card.

Anatomy

  • .gift-card__preview: the __card (credit card proportions) with the __brand, __amount, __to and __message, updated as the customer types; sticky from the md breakpoint.
  • .gift-card__form: the __amounts fieldset (radio __options and the __custom amount field), the recipient __row, the message with a character counter and the delivery date.

Accessibility and Usability

  • The preview is decorative (aria-hidden), because it only repeats the form fields; screen reader users get the same information from the form itself.
  • The amounts are native radio buttons in a fieldset with a legend. Choosing "Other" shows the custom amount field and moves the focus into it.
  • The custom amount shows its range in the label ("$10–$500") and validates it with min and max; the $ sign is a visual prefix inside the field.
  • The message counter is part of the label, so it's read with the field; maxlength enforces the limit.

Technical Details

  • Without JavaScript, every field is visible, and the form posts normally; the script only toggles the custom field, sets its required and updates the preview.
  • The card colors come from the primary and secondary tokens, so the preview follows the active style.
  • Validate the amount and the date on the server too.

Documentation

Learn about Spruce CSS through our extensive documentation.

Components

Explore our UI library of accessible components built with Spruce CSS.

Blocks and Pages

Full-width page blocks and complete page templates, ready to copy.

Find us on GitHub

Did you find a bug? Have an idea or a question? Please open an issue to help us develop the project.