Size Guide

Size guide

Size guide: tops

Body measurements by size, in centimeters
SizeChestWaistHips
XS82–8666–7088–92
S87–9271–7693–98
M93–10077–8499–106
L101–10885–92107–114
XL109–11893–102115–124

How to measure

Chest
Around the fullest part of your chest, under your arms.
Waist
Around your natural waistline, above your belly button.
Hips
Around the widest part of your hips, with your feet together.

Between two sizes? Choose the larger one for a relaxed fit.

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

.size-guide {
  background-color: color('surface');
  border: config('border-width-base', $display) solid color('border');
  border-radius: radius('lg');
  box-shadow: shadow('xl');
  inline-size: min(36rem, calc(100vi - #{spacer('m')} * 2));
  max-block-size: calc(100dvb - #{spacer('m')} * 2);
  max-inline-size: none;
  padding: 0;

  &::backdrop {
    background-color: color('overlay');
  }

  @include motion-safe {
    &[open] {
      animation: size-guide-in 0.2s ease-out;
    }
  }

  &__header {
    align-items: center;
    background-color: color('surface');
    border-block-end: config('border-width-base', $display) solid color('border');
    display: flex;
    inset-block-start: 0;
    justify-content: space-between;
    padding: spacer('xs') spacer('xs') spacer('xs') spacer('m');
    position: sticky;
    z-index: 1;
  }

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

  &__body {
    padding: spacer('m');
    @include layout-stack('m');
  }

  // A two-option segmented control from native radio buttons.
  &__units {
    background-color: color('surface-muted');
    border: config('border-width-base', $display) solid color('border');
    border-radius: radius('pill');
    display: inline-flex;
    padding: 3px;

    label {
      cursor: pointer;
      position: relative;
    }

    input {
      appearance: none;
      border-radius: radius('pill');
      cursor: pointer;
      inset: 0;
      margin: 0;
      // Invisible but focusable and clickable; the label shows the state.
      opacity: 0;
      position: absolute;
    }

    span {
      border-radius: radius('pill');
      color: color('muted');
      display: block;
      font-size: config('font-size-sm', $typography);
      font-weight: 600;
      padding: spacer('xxs') spacer('s');
      @include transition;
    }

    label:has(:checked) span {
      background-color: color('surface');
      box-shadow: shadow('sm');
      color: color('heading');
    }

    label:has(:focus-visible) span {
      outline: 2px solid color('focus-ring');
      outline-offset: 1px;
    }
  }

  &__table {
    font-variant-numeric: tabular-nums;
    margin: 0;
    // Four short columns fit a phone; no need for the responsive minimum width.
    min-inline-size: 0 !important;

    td,
    th {
      min-inline-size: 0;
      padding-inline: spacer('xs');
      text-align: center;
      white-space: nowrap;
    }

    th[scope='row'] {
      color: color('heading');
    }
  }

  &__subtitle {
    font-size: font-size('h5');
  }

  &__measure {
    @include layout-stack('s');

    dl {
      margin-inline: 0;
      @include layout-stack('xs');
    }

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

    dd {
      margin: 0;
    }
  }

  &__tip {
    background-color: color-alpha('info', 0.12, 'alert');
    border-radius: radius('md');
    font-size: config('font-size-sm', $typography);
    padding: spacer('xs') spacer('s');
  }
}

[dir='rtl'] .size-guide__header {
  padding: spacer('xs') spacer('m') spacer('xs') spacer('xs');
}

@keyframes size-guide-in {
  from {
    opacity: 0;
    translate: 0 1rem;
  }
}
<a class="btn btn--outline-secondary btn--sm" href="#" data-size-guide-open="size-guide-tops">
    <svg class="btn__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M21.3 15.3a2.4 2.4 0 0 1 0 3.4l-2.6 2.6a2.4 2.4 0 0 1-3.4 0L2.7 8.7a2.41 2.41 0 0 1 0-3.4l2.6-2.6a2.41 2.41 0 0 1 3.4 0Z"/><path d="m14.5 12.5 2-2"/><path d="m11.5 9.5 2-2"/><path d="m8.5 6.5 2-2"/><path d="m17.5 15.5 2-2"/></svg>
    Size guide
</a>
<dialog class="size-guide" id="size-guide-tops" aria-labelledby="size-guide-title">
    <header class="size-guide__header">
        <h2 class="size-guide__title" id="size-guide-title">Size guide: tops</h2>
        <button class="btn btn--ghost-secondary btn--icon btn--sm" type="button" data-size-guide-close>
            <svg class="btn__icon" 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="M18 6 6 18M6 6l12 12"/></svg>
            <span class="sr-only">Close</span>
        </button>
    </header>
    <div class="size-guide__body">
        <div class="size-guide__units" role="radiogroup" aria-label="Units">
            <label><input type="radio" name="size-guide-unit" value="cm" checked data-size-guide-unit><span>cm</span></label>
            <label><input type="radio" name="size-guide-unit" value="in" data-size-guide-unit><span>inches</span></label>
        </div>
        <div class="table-responsive">
            <table class="table table--sm size-guide__table" data-size-guide-table>
                <caption class="sr-only">Body measurements by size, in <span data-size-guide-caption>centimeters</span></caption>
                <thead>
                    <tr><th scope="col">Size</th><th scope="col">Chest</th><th scope="col">Waist</th><th scope="col">Hips</th></tr>
                </thead>
                <tbody>
                    <tr><th scope="row">XS</th><td data-cm="82-86">82–86</td><td data-cm="66-70">66–70</td><td data-cm="88-92">88–92</td></tr>
                    <tr><th scope="row">S</th><td data-cm="87-92">87–92</td><td data-cm="71-76">71–76</td><td data-cm="93-98">93–98</td></tr>
                    <tr><th scope="row">M</th><td data-cm="93-100">93–100</td><td data-cm="77-84">77–84</td><td data-cm="99-106">99–106</td></tr>
                    <tr><th scope="row">L</th><td data-cm="101-108">101–108</td><td data-cm="85-92">85–92</td><td data-cm="107-114">107–114</td></tr>
                    <tr><th scope="row">XL</th><td data-cm="109-118">109–118</td><td data-cm="93-102">93–102</td><td data-cm="115-124">115–124</td></tr>
                </tbody>
            </table>
        </div>
        <section class="size-guide__measure" aria-labelledby="size-guide-measure">
            <h3 class="size-guide__subtitle" id="size-guide-measure">How to measure</h3>
            <dl>
                <div><dt>Chest</dt><dd>Around the fullest part of your chest, under your arms.</dd></div>
                <div><dt>Waist</dt><dd>Around your natural waistline, above your belly button.</dd></div>
                <div><dt>Hips</dt><dd>Around the widest part of your hips, with your feet together.</dd></div>
            </dl>
            <p class="size-guide__tip">Between two sizes? Choose the larger one for a relaxed fit.</p>
        </section>
    </div>
</dialog>
(() => {
  const toInches = (cm) => (cm / 2.54).toFixed(1).replace(/\.0$/, '');

  document.querySelectorAll('[data-size-guide-open]').forEach((opener) => {
    const dialog = document.getElementById(opener.dataset.sizeGuideOpen);

    if (!dialog) {
      return;
    }

    const caption = dialog.querySelector('[data-size-guide-caption]');

    // Without JavaScript, the opener is a link to the size guide page.
    opener.setAttribute('role', 'button');
    opener.setAttribute('aria-haspopup', 'dialog');

    const open = (event) => {
      event.preventDefault();
      dialog.showModal();
    };

    opener.addEventListener('click', open);
    opener.addEventListener('keydown', (event) => {
      if (event.key === ' ') {
        open(event);
      }
    });

    dialog.addEventListener('click', (event) => {
      if (event.target === dialog || event.target.closest('[data-size-guide-close]')) {
        dialog.close();
      }
    });

    // The table stores the centimeters; inches are calculated from them.
    dialog.addEventListener('change', (event) => {
      if (!event.target.matches('[data-size-guide-unit]')) {
        return;
      }

      const inches = event.target.value === 'in';

      dialog.querySelectorAll('[data-cm]').forEach((cell) => {
        const [from, to] = cell.dataset.cm.split('-').map(Number);
        cell.textContent = inches ? `${toInches(from)}–${toInches(to)}` : `${from}–${to}`;
      });

      caption.textContent = inches ? 'inches' : 'centimeters';
    });
  });
})();

Size Guide

A size chart in a modal dialog with a centimeter/inch switch and measuring instructions.

Anatomy

  • [data-size-guide-open]: the opener, a link to the size guide page with the id of the dialog.
  • .size-guide: a native dialog with a sticky __header and the __body.
  • .size-guide__units: a segmented control from two native radio buttons.
  • .size-guide__table: the chart; every cell stores its range in centimeters (data-cm="87-92").
  • .size-guide__measure: a description list with the measuring instructions and a __tip.

Accessibility and Usability

  • showModal() traps the focus in the dialog, closes it with Escape and returns the focus to the opener. A click on the backdrop closes it too.
  • The unit switch is a labelled radio group (role="radiogroup", aria-label="Units") of native radio buttons, so the arrow keys work and screen readers announce "cm, radio button, 1 of 2".
  • The table has a caption that says the current unit, and row and column headers.
  • Progressive enhancement: the opener is a link to a size guide page; with JavaScript, it opens the dialog instead.

Technical Details

  • The centimeters are the source of truth; the inches are calculated from them (cm / 2.54, one decimal), so the two can't get out of sync.
  • Default to the unit of the shopper's locale (for example, inches for en-US) by checking the right radio button on the server.
  • Link it from the size option of the Product Details.

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.