Address Book

Addresses

  • Home

    Default
    Amelia Brooks
    24 Juniper Lane, Apt 3B
    Portland, OR 97205
    United States
    +1 (555) 010-2030
    Edit Home
  • Office

    Default
    Amelia Brooks
    Cone Studio, 410 Market Street
    San Francisco, CA 94105
    United States
    +1 (555) 010-4410
    Edit Office
  • Mom

    Default
    Helen Brooks
    8 Orchard Road
    Burlington, VT 05401
    United States
    Edit Mom
  • Add a new address

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

.address-book {
  @include layout-stack('m');

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

  &__list {
    display: grid;
    gap: spacer('m');
    grid-template-columns: repeat(auto-fill, minmax(min(16rem, 100%), 1fr));
    list-style: none;
    padding: 0;

    // The items sit side by side: clear the stack margins of the base list style.
    > li {
      margin-block: 0;
      padding-block: 0;
    }
  }

  &__item {
    display: flex;
  }

  &__card {
    background-color: color('surface');
    border: config('border-width-base', $display) solid color('border');
    border-radius: radius('lg');
    display: flex;
    flex-direction: column;
    gap: spacer('s');
    inline-size: 100%;
    padding: spacer('m');
  }

  &__item--default &__card {
    border-color: color('primary');
    box-shadow: 0 0 0 1px color('primary');
  }

  &__header {
    align-items: center;
    display: flex;
    gap: spacer('xs');
    justify-content: space-between;
  }

  &__label {
    font-size: font-size('h6');
  }

  // Only the default address shows the badge and hides the "Set as default" and "Delete" buttons.
  &__badge,
  &__item--default &__make-default,
  &__item--default &__delete {
    display: none;
  }

  &__item--default &__badge {
    display: inline-flex;
  }

  &__address {
    color: color('text');
    flex-grow: 1;
    font-size: config('font-size-sm', $typography);
    font-style: normal;

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

  &__phone {
    color: color('muted');
    display: inline-block;
    margin-block-start: spacer('xxs');
  }

  &__actions {
    border-block-start: config('border-width-base', $display) solid color('border');
    display: flex;
    flex-wrap: wrap;
    gap: spacer('xxs');
    margin-inline: calc(#{spacer('xs')} * -1);
    padding-block-start: spacer('xs');
  }

  &__add {
    align-items: center;
    border: 2px dashed color('border');
    border-radius: radius('lg');
    color: color('primary');
    display: flex;
    flex-direction: column;
    font-weight: 600;
    gap: spacer('xs');
    inline-size: 100%;
    justify-content: center;
    min-block-size: 12rem;
    padding: spacer('m');
    text-decoration: none;

    &:hover {
      background-color: color-alpha('primary', 0.05);
      border-color: color('primary');
    }

    svg {
      block-size: 1.75rem;
      inline-size: 1.75rem;
    }
  }
}
<section class="address-book" aria-labelledby="address-book-title" data-address-book>
    <h2 class="address-book__title" id="address-book-title">Addresses</h2>
    <ul class="address-book__list" role="list" data-address-book-list>
        <li class="address-book__item address-book__item--default" data-address>
            <article class="address-book__card" aria-labelledby="address-1">
                <header class="address-book__header">
                    <h3 class="address-book__label" id="address-1">Home</h3>
                    <span class="badge badge--primary address-book__badge">Default</span>
                </header>
                <address class="address-book__address">
                    <strong>Amelia Brooks</strong><br>
                    24 Juniper Lane, Apt 3B<br>
                    Portland, OR 97205<br>
                    United States<br>
                    <span class="address-book__phone">+1 (555) 010-2030</span>
                </address>
                <div class="address-book__actions">
                    <a class="btn btn--ghost-primary btn--sm" href="#">Edit<span class="sr-only"> Home</span></a>
                    <button class="btn btn--ghost-primary btn--sm address-book__make-default" type="button" data-address-default>Set as default<span class="sr-only">: Home</span></button>
                    <button class="btn btn--ghost-danger btn--sm address-book__delete" type="button" data-address-delete>Delete<span class="sr-only"> Home</span></button>
                </div>
            </article>
        </li>
        <li class="address-book__item" data-address>
            <article class="address-book__card" aria-labelledby="address-2">
                <header class="address-book__header">
                    <h3 class="address-book__label" id="address-2">Office</h3>
                    <span class="badge badge--primary address-book__badge">Default</span>
                </header>
                <address class="address-book__address">
                    <strong>Amelia Brooks</strong><br>
                    Cone Studio, 410 Market Street<br>
                    San Francisco, CA 94105<br>
                    United States<br>
                    <span class="address-book__phone">+1 (555) 010-4410</span>
                </address>
                <div class="address-book__actions">
                    <a class="btn btn--ghost-primary btn--sm" href="#">Edit<span class="sr-only"> Office</span></a>
                    <button class="btn btn--ghost-primary btn--sm address-book__make-default" type="button" data-address-default>Set as default<span class="sr-only">: Office</span></button>
                    <button class="btn btn--ghost-danger btn--sm address-book__delete" type="button" data-address-delete>Delete<span class="sr-only"> Office</span></button>
                </div>
            </article>
        </li>
        <li class="address-book__item" data-address>
            <article class="address-book__card" aria-labelledby="address-3">
                <header class="address-book__header">
                    <h3 class="address-book__label" id="address-3">Mom</h3>
                    <span class="badge badge--primary address-book__badge">Default</span>
                </header>
                <address class="address-book__address">
                    <strong>Helen Brooks</strong><br>
                    8 Orchard Road<br>
                    Burlington, VT 05401<br>
                    United States<br>
                </address>
                <div class="address-book__actions">
                    <a class="btn btn--ghost-primary btn--sm" href="#">Edit<span class="sr-only"> Mom</span></a>
                    <button class="btn btn--ghost-primary btn--sm address-book__make-default" type="button" data-address-default>Set as default<span class="sr-only">: Mom</span></button>
                    <button class="btn btn--ghost-danger btn--sm address-book__delete" type="button" data-address-delete>Delete<span class="sr-only"> Mom</span></button>
                </div>
            </article>
        </li>
        <li class="address-book__item address-book__item--add">
            <a class="address-book__add" href="#">
                <svg 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="M12 5v14M5 12h14"/></svg>
                Add a new address
            </a>
        </li>
    </ul>
    <p class="sr-only" aria-live="polite" data-address-book-status></p>
</section>
(() => {
  document.querySelectorAll('[data-address-book]').forEach((book) => {
    const list = book.querySelector('[data-address-book-list]');
    const status = book.querySelector('[data-address-book-status]');
    const defaultClass = 'address-book__item--default';

    const labelOf = (item) => item.querySelector('.address-book__label').textContent;

    list.addEventListener('click', (event) => {
      const item = event.target.closest('[data-address]');

      if (!item) return;

      if (event.target.closest('[data-address-default]')) {
        list.querySelector(`.${defaultClass}`)?.classList.remove(defaultClass);
        item.classList.add(defaultClass);
        // The default address goes first.
        list.prepend(item);
        status.textContent = `${labelOf(item)} is now your default address.`;
        item.querySelector('a').focus();
      }

      if (event.target.closest('[data-address-delete]')) {
        if (!window.confirm(`Delete the ${labelOf(item)} address?`)) return;

        const next = item.nextElementSibling;

        item.remove();
        status.textContent = `The ${labelOf(item)} address is deleted.`;
        next.querySelector('a').focus();
      }
    });
  });
})();

Address Book

The saved addresses of a customer account, with the default address marked and actions to edit, delete or make another address the default.

Anatomy

  • .address-book__item: an article card with the __label, the address element and the __actions. --default highlights the card, shows the "Default" Badge and hides the "Set as default" and "Delete" buttons.
  • .address-book__item--add: a dashed card that links to the new address form.

Accessibility and Usability

  • The address uses the address element (a contact address, which is what this is) with the name first, like on an envelope.
  • The action buttons have hidden text with the address label ("Delete Office"), so each is unique.
  • The default address can't be deleted; make another one the default first. This avoids an account without a default address at checkout.
  • Deleting asks for confirmation; after it, the focus moves to the next card's link. Both changes are announced in a polite live region.
  • Making an address the default moves it to the first place and keeps the focus on it.

Technical Details

  • The default state is one class: the badge and the button visibility follow it in CSS, so the script only moves the class.
  • Without JavaScript, point the buttons to forms that post to the server (or make them links to confirmation pages).
  • Replace the window.confirm() with a Modal for a consistent look.

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.