Returns Form

Return items

Order #10482 · Free returns until

1. What would you like to return?
Sand · M · $79
Navy · $89
Matte white · $42
2. How would you like your refund?

Select the items to return.

@use 'sprucecss/scss/spruce' as *;

.returns-form {
  background-color: color('surface');
  border: config('border-width-base', $display) solid color('border');
  border-radius: radius('lg');
  padding: spacer('m');
  @include layout-stack('m');

  &__header {
    @include layout-stack('xxs');
  }

  &__title {
    font-size: font-size('h4');
  }

  &__text {
    color: color('muted');
    font-size: config('font-size-sm', $typography);
  }

  &__items,
  &__methods {
    border: 0;
    margin: 0;
    padding: 0;
    @include layout-stack('xs');
  }

  &__legend {
    color: color('heading');
    font-size: config('font-size-lg', $typography);
    font-weight: 700;
    margin-block-end: spacer('xs');
    padding: 0;
  }

  &__item {
    align-items: center;
    border: config('border-width-base', $display) solid color('border');
    border-radius: radius('md');
    column-gap: spacer('s');
    display: grid;
    grid-template-areas:
      'check image name'
      'check image meta'
      'reason reason reason';
    grid-template-columns: auto 4rem minmax(0, 1fr);
    padding: spacer('s');

    &:has(input:checked) {
      border-color: color('primary');
      box-shadow: 0 0 0 1px color('primary');
    }

    img {
      aspect-ratio: 1;
      border-radius: radius('sm');
      grid-area: image;
      object-fit: cover;
    }
  }

  &__check {
    grid-area: check;
  }

  &__name {
    align-self: end;
    color: color('heading');
    cursor: pointer;
    font-weight: 600;
    grid-area: name;
  }

  &__meta {
    align-self: start;
    color: color('muted');
    font-size: config('font-size-sm', $typography);
    grid-area: meta;
  }

  &__reason {
    display: grid;
    gap: spacer('xxs');
    grid-area: reason;
    margin-block-start: spacer('s');
  }

  // The reason appears for the selected items only (where :has() works; otherwise it's always visible).
  @supports selector(:has(*)) {
    &__item:not(:has(.returns-form__check:checked)) &__reason {
      display: none;
    }
  }

  &__method {
    align-items: flex-start;
    border: config('border-width-base', $display) solid color('border');
    border-radius: radius('md');
    cursor: pointer;
    display: flex;
    gap: spacer('s');
    padding: spacer('s');

    &:has(input:checked) {
      border-color: color('primary');
      box-shadow: 0 0 0 1px color('primary');
    }

    input {
      margin-block-start: 0.2em;
    }

    span {
      color: color('muted');
      font-size: config('font-size-sm', $typography);
    }

    strong {
      color: color('heading');
      display: block;
      font-size: config('font-size-base', $typography);
    }
  }

  &__footer {
    align-items: center;
    border-block-start: config('border-width-base', $display) solid color('border');
    display: flex;
    flex-wrap: wrap;
    gap: spacer('s');
    justify-content: space-between;
    padding-block-start: spacer('m');
  }

  &__summary {
    margin: 0;

    strong {
      color: color('heading');
    }
  }

  > * + fieldset {
    margin-block-start: spacer('m');
  }
}
<form class="returns-form" action="#" method="post" aria-labelledby="returns-form-title" data-returns-form>
    <header class="returns-form__header">
        <h2 class="returns-form__title" id="returns-form-title">Return items</h2>
        <p class="returns-form__text">Order #10482 · Free returns until <time datetime="2026-11-03">November 3</time></p>
    </header>
    <fieldset class="returns-form__items">
        <legend class="returns-form__legend">1. What would you like to return?</legend>
        <div class="form-check returns-form__item" data-returns-item data-price="79">
            <input class="form-check__control returns-form__check" id="return-item-1" name="items[]" type="checkbox" value="1" aria-describedby="return-item-1-meta">
            <img src="https://images.unsplash.com/photo-1596755094514-f87e34085b2c?auto=format&amp;fit=crop&amp;w=160&amp;q=70" alt="" width="64" height="64" loading="lazy">
            <label class="returns-form__name" for="return-item-1">Linen Overshirt</label>
            <span class="returns-form__meta" id="return-item-1-meta">Sand · M · $79</span>
            <div class="returns-form__reason" data-returns-reason>
                <label class="form-label" for="return-reason-1">Reason <span class="sr-only">for returning Linen Overshirt</span></label>
                <select class="form-control form-control--sm" id="return-reason-1" name="reason[1]">
                    <option value="">Choose a reason</option>
                    <option>Too small</option>
                    <option>Too big</option>
                    <option>Not as described</option>
                    <option>Arrived damaged</option>
                    <option>Changed my mind</option>
                </select>
            </div>
        </div>
        <div class="form-check returns-form__item" data-returns-item data-price="89">
            <input class="form-check__control returns-form__check" id="return-item-2" name="items[]" type="checkbox" value="2" aria-describedby="return-item-2-meta">
            <img src="https://images.unsplash.com/photo-1553062407-98eeb64c6a62?auto=format&amp;fit=crop&amp;w=160&amp;q=70" alt="" width="64" height="64" loading="lazy">
            <label class="returns-form__name" for="return-item-2">Everyday Backpack</label>
            <span class="returns-form__meta" id="return-item-2-meta">Navy · $89</span>
            <div class="returns-form__reason" data-returns-reason>
                <label class="form-label" for="return-reason-2">Reason <span class="sr-only">for returning Everyday Backpack</span></label>
                <select class="form-control form-control--sm" id="return-reason-2" name="reason[2]">
                    <option value="">Choose a reason</option>
                    <option>Too small</option>
                    <option>Too big</option>
                    <option>Not as described</option>
                    <option>Arrived damaged</option>
                    <option>Changed my mind</option>
                </select>
            </div>
        </div>
        <div class="form-check returns-form__item" data-returns-item data-price="42">
            <input class="form-check__control returns-form__check" id="return-item-3" name="items[]" type="checkbox" value="3" aria-describedby="return-item-3-meta">
            <img src="https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?auto=format&amp;fit=crop&amp;w=160&amp;q=70" alt="" width="64" height="64" loading="lazy">
            <label class="returns-form__name" for="return-item-3">Ceramic Pour-Over</label>
            <span class="returns-form__meta" id="return-item-3-meta">Matte white · $42</span>
            <div class="returns-form__reason" data-returns-reason>
                <label class="form-label" for="return-reason-3">Reason <span class="sr-only">for returning Ceramic Pour-Over</span></label>
                <select class="form-control form-control--sm" id="return-reason-3" name="reason[3]">
                    <option value="">Choose a reason</option>
                    <option>Too small</option>
                    <option>Too big</option>
                    <option>Not as described</option>
                    <option>Arrived damaged</option>
                    <option>Changed my mind</option>
                </select>
            </div>
        </div>
    </fieldset>
    <fieldset class="returns-form__methods">
        <legend class="returns-form__legend">2. How would you like your refund?</legend>
        <label class="form-check returns-form__method">
            <input class="form-check__control" type="radio" name="refund" value="original" checked data-bonus="0">
            <span><strong>Original payment method</strong> 3–5 business days after we receive the items</span>
        </label>
        <label class="form-check returns-form__method">
            <input class="form-check__control" type="radio" name="refund" value="credit" data-bonus="10">
            <span><strong>Store credit + 10% bonus</strong> Instantly, once the return is scanned</span>
        </label>
    </fieldset>
    <footer class="returns-form__footer">
        <p class="returns-form__summary" aria-live="polite" data-returns-summary>Select the items to return.</p>
        <button class="btn btn--primary" type="submit">Continue to shipping label</button>
    </footer>
</form>
(() => {
  document.querySelectorAll('[data-returns-form]').forEach((form) => {
    const items = [...form.querySelectorAll('[data-returns-item]')];
    const summary = form.querySelector('[data-returns-summary]');

    const selected = () => items.filter((item) => item.querySelector('[type="checkbox"]').checked);

    const update = () => {
      const chosen = selected();
      const bonus = Number(form.querySelector('[name="refund"]:checked').dataset.bonus);
      const total = chosen.reduce((sum, item) => sum + Number(item.dataset.price), 0) * (1 + bonus / 100);

      items.forEach((item) => {
        const select = item.querySelector('select');

        // A selected item needs a reason; the select is only shown (and required) then.
        select.required = item.querySelector('[type="checkbox"]').checked;
      });

      summary.innerHTML = chosen.length
        ? `${chosen.length} ${chosen.length === 1 ? 'item' : 'items'}, estimated refund: <strong>$${total.toFixed(2)}</strong>`
        : 'Select the items to return.';
    };

    form.addEventListener('change', update);

    form.addEventListener('submit', (event) => {
      if (selected().length === 0) {
        event.preventDefault();
        summary.textContent = 'Select at least one item to return.';
        items[0].querySelector('[type="checkbox"]').focus();
      }
    });

    update();
  });
})();

Returns Form

A return request form: choose the items of an order, give a reason for each, pick the refund method and see the estimated refund.

Anatomy

  • .returns-form__items: a fieldset of __items; each has a checkbox, an image, the __name label, the __meta and a __reason select that appears when the item is selected.
  • .returns-form__methods: the refund options as large radio __method cards.
  • .returns-form__footer: the live __summary and the submit button.

Accessibility and Usability

  • The steps are two fieldsets with numbered legends, so screen reader users hear the question before the options.
  • The product name is the checkbox label, and the variant and price are linked as its description (aria-describedby).
  • The reason selects have hidden context ("Reason for returning Linen Overshirt"), and they're required only for the selected items.
  • The summary is a polite live region: it says how many items are selected and the estimated refund, and the submit error ("Select at least one item") appears there with the focus on the first checkbox.

Technical Details

  • The selected cards and the reason fields use :has(:checked), so the visual state needs no JavaScript; the script only calculates the refund and sets required.
  • The store credit option adds its data-bonus percentage to the estimate. Calculate the real refund on the server.
  • Without JavaScript, the form still posts everything; the reasons are visible in browsers without :has() support.

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.