Shopify Size Chart Without an App (Metaobject + Modal)

Shopify size chart without an app: a size-chart app costs $8 to $27 a month plus a JS widget, versus a metaobject and a modal which is free, no app, reusable and accessible

TL;DR: Size and fit is the top reason apparel gets returned, and a size chart is the cheapest way to cut that before checkout. You do not need a $7.99-to-$27-a-month app for one. Here is the reusable, accessible way: store the chart in a metaobject (define it once, attach it to many products), then show it in a small, properly accessible modal. Real HTML table, no app, no monthly fee, and it beats the usual tutorials on the two things they skip: reusability and accessibility.

The business case is simple. The NRF puts US ecommerce returns at 19.3 percent of online sales in 2025, and a 2023 Coresight Research survey commissioned by fit-tech vendor 3DLOOK found 53 percent of apparel brands and retailers named size or fit as the top reason for returns. A size chart is a direct, one-time fix for the single most common return reason, and one of the cheapest conversion wins on a product page. Paying a subscription for it is the part that does not add up.

Does Shopify have a native size chart?

No, and it is worth being precise, because the confusion sends people to the App Store. Shopify has no size-chart feature you toggle on. Its own pop-up size chart tutorial has you build one: create a metafield, attach a page, and wire it to the theme editor’s popup block, and it says plainly that this needs a theme with dynamic sources, with code edits for older themes. In other words, the building blocks are there, but Shopify does not hand you the recipe.

The right building block is the metaobject. Shopify’s developer docs define metaobjects as structured data you can “reference and reuse across your store,” and they explicitly list product size charts with multiple measurements as a use case. That reuse is the whole point: a metaobject lets you create one “Men’s tops” chart and attach it to every men’s top, instead of pasting the same image into 40 products. Every other tutorial I checked stops at a per-product image or page metafield, which does not scale and cannot hold a real table.

Why not just use a size chart app?

Because you would be renting a platform to get one table. The popular options are good apps: Kiwi Sizing is free for up to 3 charts, then $7.99 a month and up to $26.99 for higher tiers, and MP Size Chart, formerly Avada, sits in a similar range. They are built for stores that need dozens of charts, an AI size recommender, multi-language, or analytics.

If that is you, use one. But if you need a single static chart on a few product types, an app is a subscription plus an extra JavaScript widget on your storefront for something a metaobject and a few lines of Liquid do for free. It is a scope mismatch. The rest of this post is the free version, and it is more accessible than the widget most apps inject.

The reusable way: store the chart in a metaobject

Set this up once in the admin. It takes about five minutes.

  • Create the metaobject: go to Settings, then Custom data, then Metaobjects, then Add definition. Name it Size chart. Add three fields: heading (single line text), table (multi-line text), and notes (multi-line text, optional). The table field holds a plain HTML table, which is what makes the chart a real, reflowing, screen-reader-readable table instead of a flat image.
  • Add a product reference: go to Settings, then Custom data, then Products, then Add definition. Create a metafield named Size chart with namespace and key custom.size_chart, and set its type to Metaobject reference, pointing at your Size chart definition. This is the link that lets you attach one chart to many products.
  • Create and attach charts: under Content, then Metaobjects, add an entry like “Men’s tops”, paste your table HTML into the table field (template below), then open any product and set its Size chart metafield to that entry.

Here is a clean, accessible table to paste into the table field and edit. A real <table> with a header row is what screen readers announce as a table, and what reflows on a phone.

<table>
  <thead>
    <tr><th>Size</th><th>Chest (in)</th><th>Waist (in)</th><th>Length (in)</th></tr>
  </thead>
  <tbody>
    <tr><td>S</td><td>36 to 38</td><td>28 to 30</td><td>28</td></tr>
    <tr><td>M</td><td>39 to 41</td><td>31 to 33</td><td>29</td></tr>
    <tr><td>L</td><td>42 to 44</td><td>34 to 36</td><td>30</td></tr>
    <tr><td>XL</td><td>45 to 47</td><td>37 to 39</td><td>31</td></tr>
  </tbody>
</table>

The code: one snippet, no app

Create snippets/size-chart.liquid and paste this in. It renders a “Size chart” button and an accessible modal, but only when the product has a chart attached, so products without one show nothing.

{%- comment -%}
  Reusable size chart, no app. Reads the size_chart metaobject linked via the
  product metafield custom.size_chart and shows it in an accessible modal.
  Render it next to your buy buttons, and pass the product:
  {% render 'size-chart', product: product %}
{%- endcomment -%}
{%- assign chart = product.metafields.custom.size_chart.value -%}
{%- if chart -%}
{%- assign sc_id = 'size-chart-' | append: product.id -%}
<button type="button" class="size-chart-btn" aria-haspopup="dialog" aria-controls="{{ sc_id }}" data-size-chart-open="{{ sc_id }}">
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M3 7h18M3 12h18M3 17h18M7 7v3M12 7v5M17 7v3"/></svg>
  Size chart
</button>

<div id="{{ sc_id }}" class="size-chart" role="dialog" aria-modal="true" aria-labelledby="{{ sc_id }}-title" hidden>
  <div class="size-chart__overlay" data-size-chart-close></div>
  <div class="size-chart__dialog" tabindex="-1">
    <div class="size-chart__head">
      <h2 id="{{ sc_id }}-title" class="size-chart__title">{{ chart.heading | default: 'Size chart' }}</h2>
      <button type="button" class="size-chart__close" data-size-chart-close aria-label="Close size chart">&times;</button>
    </div>
    <div class="size-chart__body">
      {{ chart.table }}
      {%- if chart.notes != blank -%}<div class="size-chart__notes">{{ chart.notes }}</div>{%- endif -%}
    </div>
  </div>
</div>

<script>
(function () {
  var id = {{ sc_id | json }};
  var modal = document.getElementById(id);
  if (!modal || modal.dataset.scInit) { return; }
  modal.dataset.scInit = '1';
  // Move the modal to <body> so a transformed ancestor can't clip the overlay.
  document.body.appendChild(modal);
  var dialog = modal.querySelector('.size-chart__dialog');
  var last = null, inerted = [];
  function tabbable() {
    return modal.querySelectorAll('a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])');
  }
  function open() {
    last = document.activeElement;
    modal.hidden = false;
    document.body.style.overflow = 'hidden';
    // Make the rest of the page inert, so screen readers cannot reach it either.
    inerted = [];
    Array.prototype.forEach.call(document.body.children, function (el) {
      if (el !== modal && !el.hasAttribute('inert')) { el.setAttribute('inert', ''); inerted.push(el); }
    });
    (tabbable()[0] || dialog).focus();
    document.addEventListener('keydown', onKey);
  }
  function close() {
    modal.hidden = true;
    document.body.style.overflow = '';
    inerted.forEach(function (el) { el.removeAttribute('inert'); });
    inerted = [];
    document.removeEventListener('keydown', onKey);
    if (last && last.focus) { last.focus(); }
  }
  function onKey(e) {
    if (e.key === 'Escape') { close(); return; }
    if (e.key !== 'Tab') { return; }
    var f = tabbable();
    if (!f.length) { return; }
    var first = f[0], lastEl = f[f.length - 1];
    if (e.shiftKey && document.activeElement === first) { e.preventDefault(); lastEl.focus(); }
    else if (!e.shiftKey && document.activeElement === lastEl) { e.preventDefault(); first.focus(); }
  }
  Array.prototype.forEach.call(document.querySelectorAll('[data-size-chart-open="' + id + '"]'), function (b) { b.addEventListener('click', open); });
  Array.prototype.forEach.call(modal.querySelectorAll('[data-size-chart-close]'), function (b) { b.addEventListener('click', close); });
})();
</script>

{% stylesheet %}
  .size-chart-btn { display: inline-flex; align-items: center; gap: 0.4rem; background: none; border: 0; padding: 0.35rem 0; font: inherit; color: currentColor; text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
  .size-chart[hidden] { display: none; }
  .size-chart { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; padding: 1rem; }
  .size-chart__overlay { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.55); }
  .size-chart__dialog { position: relative; background: #ffffff; color: #141414; width: min(560px, 100%); max-height: 85vh; overflow: auto; border-radius: 12px; padding: 1.25rem 1.4rem; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35); }
  .size-chart__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; margin-bottom: 0.75rem; }
  .size-chart__title { margin: 0; font-size: 1.15rem; }
  .size-chart__close { background: none; border: 0; font-size: 1.7rem; line-height: 1; cursor: pointer; color: inherit; padding: 0 0.25rem; }
  .size-chart__body table { width: 100%; border-collapse: collapse; }
  .size-chart__body th, .size-chart__body td { border: 1px solid rgba(0, 0, 0, 0.12); padding: 0.5rem 0.65rem; text-align: left; }
  .size-chart__notes { margin-top: 0.9rem; font-size: 0.92rem; opacity: 0.8; }
{% endstylesheet %}
{%- endif -%}

Then render it once on the product page, next to the variant picker or the add-to-cart button, where a shopper looks for it: add {% render 'size-chart', product: product %} to your product template or main product section. Do this on a duplicate or development theme first, never the live one.

How do I make the size chart popup accessible?

This is the part every thin tutorial skips, and it is where a size-chart popup either works for everyone or traps keyboard and screen-reader users. The code above follows the WAI-ARIA dialog pattern:

  • Announced as a dialog: role="dialog" and aria-modal="true" tell assistive tech that everything behind it is inert, and aria-labelledby points at the visible title so the dialog has a name.
  • Focus moves in, and stays: when the button is pressed, focus moves into the dialog, and Tab and Shift+Tab wrap around inside it instead of leaking back to the page behind.
  • Escape closes, focus returns: pressing Escape or the close button dismisses the modal and returns focus to the “Size chart” button that opened it, so a keyboard user does not lose their place.

None of that needs a library. It is about 30 lines of vanilla JavaScript, and it is the difference between a popup that passes an accessibility audit and one that fails it.

How do I keep it fast?

There is almost nothing to keep fast, which is the point. The chart data comes from Liquid at render time, so there is no app SDK and no network request. The only JavaScript is the modal behavior, which runs on click, not on load, so it costs nothing on your Largest Contentful Paint. The modal starts with the hidden attribute, so it never flashes or shifts layout. Compared to a size-chart app that injects a widget on every product page, this is the version that shows up as a rounding error in your Core Web Vitals. It is the same pattern as building recently viewed products without an app.

Download the Size Chart Code Cheat Sheet (PDF)

The takeaway

  • Shopify has no native size chart. The building blocks (metaobjects) are there, but you assemble it yourself, which is why people reach for an app.
  • Size and fit is the leading reason apparel gets returned, so a clear chart is a direct CRO win against your most common return, and there is no reason to pay monthly for it.
  • Store the chart in a metaobject and reference it from a product metafield, so one chart attaches to many products. That reuse is what per-product image tutorials miss.
  • Use a real HTML table, not an image, so the chart reflows on mobile and screen readers can read it.
  • Make the modal accessible: role, focus trap, Escape, and focus return. It is 30 lines of vanilla JS, and it is the bar most size-chart tutorials fail.

I keep writing these because the App Store’s answer to a one-table feature is a subscription and a widget, and the free version is usually both faster and more accessible. If there is another app you are paying for that you suspect is a snippet in disguise, tell me and I will take it apart.

Frequently Asked Questions

Does Shopify have a native size chart feature?

No. Shopify has no size-chart component you can toggle on. Its own help tutorial has you build one from a metafield plus the theme editor’s popup block, and it needs an Online Store 2.0 theme with dynamic sources. The clean, reusable way is a metaobject: Shopify’s developer docs list product size charts as a metaobject use case. You store the chart once and reference it from many products, no app required.

How do I add a size chart to Shopify without an app?

Create a size-chart metaobject in Settings then Custom data, give it a heading and a table field, then add a product metafield that references it so you can attach one chart to many products. Then render a small snippet on the product page that opens the chart in an accessible modal. The chart is a real HTML table, so it reflows on mobile and screen readers can read it, and there is nothing to pay for.

Why not just use a size chart app like Kiwi Sizing?

For a single static chart on a few product types, a size-chart app is a scope mismatch, not a necessity. Apps like Kiwi Sizing (free for 3 charts, then $7.99 to $26.99 a month) and MP Size Chart are built for stores that need dozens of charts, an AI size recommender, multi-language, or analytics. For one table, a metaobject and a few lines of Liquid do the job for free, without the widget or the subscription.

Do size charts reduce returns?

Size and fit is the leading reason apparel gets returned: a 2023 Coresight Research survey commissioned by fit-tech vendor 3DLOOK found 53 percent of apparel brands and retailers named size or fit as the top return reason. With the NRF putting US ecommerce returns at 19.3 percent of online sales in 2025, a clear size chart is one of the cheapest ways to cut the most common return reason before it happens. That is a strong CRO reason to add one, and no reason to pay monthly for it.

How do I make a size chart popup accessible?

Follow the WAI-ARIA dialog pattern: give the popup role=‘dialog’ and aria-modal=‘true’, label it with its title via aria-labelledby, move focus into it when it opens, trap Tab inside it, close it on Escape, and return focus to the button that opened it. The code below does all of that. Most size-chart tutorials skip this entirely, which leaves keyboard and screen-reader users stuck in a popup they cannot navigate or close.
Book Strategy Call