{%- comment -%} MIT License · (c) 2026 Lintel · https://lintelagency.com/resources/shopify-sections {%- endcomment -%} {%- comment -%} Size chart: a size table with measuring notes and an optional how-to-measure image, shown inline or behind a button that opens it in a native as a side drawer or a centred modal. The table is typed in the section (one row per line, cells separated by |, first line = header) or comes from a page. Optional cm / in switch converts numeric cells (also ranges like 86-90); without JavaScript the typed values show. Modern browsers open the dialog without JavaScript too (button commands); the script adds it elsewhere, plus backdrop click and the unit switch. Depends on: snippets/lib-image.liquid, snippets/lib-button.liquid {%- endcomment -%} {%- liquid assign s = section.settings assign heading_tag = s.heading_tag | default: 'h2' assign display = s.display | default: 'drawer' assign uid = 'LibSizeChart-' | append: section.id assign heading_id = uid | append: '-title' assign eager = false if section.index <= 2 and display == 'inline' assign eager = true endif assign rows = '' if s.source == 'table' and s.table != blank assign rows = s.table | newline_to_br | split: '
' endif assign has_page = false if s.source == 'page' and s.page != blank and s.page.content != blank assign has_page = true endif assign heading_text = s.heading | strip_html | strip if heading_text == blank assign heading_text = s.trigger_label | escape endif -%} {%- capture chart -%}
{%- if s.unit_toggle -%} {%- endif -%} {%- if rows != blank -%}
{%- if s.caption != blank -%} {%- endif -%} {%- liquid assign header = '' for row in rows assign line = row | strip if line != blank assign header = line break endif endfor -%} {%- assign cells = header | split: '|' -%} {%- for cell in cells -%} {%- endfor -%} {%- assign header_seen = false -%} {%- for row in rows -%} {%- assign line = row | strip -%} {%- if line == blank -%}{%- continue -%}{%- endif -%} {%- if header_seen == false -%} {%- assign header_seen = true -%} {%- continue -%} {%- endif -%} {%- assign cells = line | split: '|' -%} {%- for cell in cells -%} {%- if forloop.first -%} {%- else -%} {%- endif -%} {%- endfor -%} {%- endfor -%}
{{ s.caption | escape }}
{{ cell | strip | escape }}
{{ cell | strip | escape }}{{ cell | strip | escape }}
{%- elsif has_page -%}
{{ s.page.content }}
{%- endif -%} {%- if s.notes != blank or s.image != blank or s.image_mobile != blank -%}
{%- if s.image != blank or s.image_mobile != blank -%}
{%- render 'lib-image', image: s.image, image_mobile: s.image_mobile, class: 'lib-size-chart__img', sizes: '(min-width: 990px) 360px, 100vw', eager: eager -%}
{%- endif -%} {%- if s.notes != blank -%}
{{ s.notes }}
{%- endif -%}
{%- endif -%}
{%- endcapture -%}
{%- if display == 'inline' -%} {%- if s.heading != blank -%} <{{ heading_tag }} class="lib-size-chart__heading lib-size-chart__heading--{{ s.heading_size }}"> {{- s.heading -}} {%- endif -%} {%- if s.text != blank -%}
{{ s.text }}
{%- endif -%} {{ chart }} {%- else -%}
{%- if s.trigger_style == 'link' -%} {%- else -%} {%- comment -%}Prints nothing. Shopify bundles every snippet stylesheet anyway; this keeps the button styles in previews that only load rendered snippets.{%- endcomment -%} {%- render 'lib-button', label: '' -%} {%- endif -%}
<{{ heading_tag }} id="{{ heading_id }}" class="lib-size-chart__heading lib-size-chart__heading--{{ s.heading_size }}"> {{- heading_text -}}
{%- if s.text != blank -%}
{{ s.text }}
{%- endif -%} {{ chart }}
{%- endif -%}
{% stylesheet %} .lib-size-chart { display: block; background-color: var(--lib-bg); background-image: var(--lib-bg-image, none); color: var(--lib-text); padding: var(--lib-pt) 1rem var(--lib-pb); } .lib-size-chart__inner { max-width: 1200px; margin: 0 auto; } .lib-size-chart--full { padding-inline: clamp(1rem, 3vw, 2.5rem); } .lib-size-chart--full .lib-size-chart__inner { max-width: none; } @media (max-width: 749px) { .lib-size-chart { padding-top: var(--lib-pt-m); padding-bottom: var(--lib-pb-m); } .lib-size-chart[data-show='desktop'] { display: none; } } @media (min-width: 750px) { .lib-size-chart[data-show='mobile'] { display: none; } } /* Heading, intro and trigger follow the alignment; the table always reads left to right. */ .lib-size-chart__inner[data-align='center'] > .lib-size-chart__heading, .lib-size-chart__inner[data-align='center'] > .lib-size-chart__text { text-align: center; margin-inline: auto; } .lib-size-chart__inner[data-align='right'] > .lib-size-chart__heading, .lib-size-chart__inner[data-align='right'] > .lib-size-chart__text { text-align: right; margin-inline-start: auto; } .lib-size-chart__inner[data-align='center'] .lib-size-chart__trigger-row { justify-content: center; } .lib-size-chart__inner[data-align='right'] .lib-size-chart__trigger-row { justify-content: flex-end; } @media (max-width: 749px) { .lib-size-chart__inner[data-align-m='left'] > .lib-size-chart__heading, .lib-size-chart__inner[data-align-m='left'] > .lib-size-chart__text { text-align: left; margin-inline: 0 auto; } .lib-size-chart__inner[data-align-m='center'] > .lib-size-chart__heading, .lib-size-chart__inner[data-align-m='center'] > .lib-size-chart__text { text-align: center; margin-inline: auto; } .lib-size-chart__inner[data-align-m='right'] > .lib-size-chart__heading, .lib-size-chart__inner[data-align-m='right'] > .lib-size-chart__text { text-align: right; margin-inline: auto 0; } .lib-size-chart__inner[data-align-m='left'] .lib-size-chart__trigger-row { justify-content: flex-start; } .lib-size-chart__inner[data-align-m='center'] .lib-size-chart__trigger-row { justify-content: center; } .lib-size-chart__inner[data-align-m='right'] .lib-size-chart__trigger-row { justify-content: flex-end; } } .lib-size-chart__heading { margin: 0; font-size: clamp(1.75rem, 3vw, 2.5rem); line-height: 1.1; letter-spacing: -0.02em; text-wrap: balance; } .lib-size-chart__heading--small { font-size: clamp(1.25rem, 2vw, 1.5rem); } .lib-size-chart__heading--large { font-size: clamp(2rem, 4vw, 3.25rem); } .lib-size-chart__heading--xlarge { font-size: clamp(2.5rem, 6vw, 4.5rem); } .lib-size-chart__text { max-width: 40rem; margin-top: 0.875rem; font-size: 1rem; line-height: 1.65; opacity: 0.85; } .lib-size-chart__text > :first-child { margin-top: 0; } .lib-size-chart__text > :last-child { margin-bottom: 0; } .lib-size-chart__inner > .lib-size-chart__body { margin-top: 2rem; } /* Trigger */ .lib-size-chart__trigger-row { display: flex; } .lib-size-chart__button { border: 0; cursor: pointer; } .lib-size-chart__link { display: inline-flex; align-items: center; gap: 0.5rem; min-height: 2.75rem; padding: 0.25rem 0; border: 0; background: none; color: inherit; font: inherit; font-weight: 500; text-decoration: underline; text-underline-offset: 0.3em; cursor: pointer; } .lib-size-chart__link svg { flex: none; color: var(--lib-accent); } .lib-size-chart__link:hover { text-decoration-thickness: 2px; } .lib-size-chart__link:focus-visible, .lib-size-chart__close:focus-visible, .lib-size-chart__unit:focus-visible, .lib-size-chart__scroll:focus-visible { outline: 2px solid var(--lib-accent); outline-offset: 3px; } /* Units */ .lib-size-chart__units { display: inline-flex; margin-bottom: 1rem; padding: 3px; border-radius: 999px; background: var(--lib-head); } .lib-size-chart__units[hidden] { display: none; } .lib-size-chart__unit { min-width: 3.25rem; min-height: 2.25rem; padding: 0.25rem 1rem; border: 0; border-radius: 999px; background: transparent; color: inherit; font: inherit; font-size: 0.875rem; font-weight: 600; cursor: pointer; } .lib-size-chart__unit[aria-pressed='true'] { background: var(--lib-accent); color: var(--lib-accent-text); } /* Table: scrolls inside its own region; the size column stays put on narrow screens. */ .lib-size-chart__scroll { max-width: 100%; overflow-x: auto; border: 1px solid var(--lib-line); border-radius: 12px; background: var(--lib-panel); } .lib-size-chart__table, .lib-size-chart__page table { width: 100%; min-width: 28rem; border-collapse: separate; border-spacing: 0; font-size: 0.9375rem; font-variant-numeric: tabular-nums; } .lib-size-chart__caption { padding: 0.875rem 1.25rem; caption-side: top; text-align: left; font-size: 0.8125rem; opacity: 0.75; border-bottom: 1px solid var(--lib-line); } .lib-size-chart__table th, .lib-size-chart__table td, .lib-size-chart__page th, .lib-size-chart__page td { padding: 0.8rem 1.25rem; text-align: left; white-space: nowrap; border-bottom: 1px solid var(--lib-line); } .lib-size-chart__table thead th, .lib-size-chart__page thead th { background: var(--lib-head); font-size: 0.8125rem; font-weight: 600; letter-spacing: 0.02em; } .lib-size-chart__table tbody tr:last-child > *, .lib-size-chart__page tbody tr:last-child > * { border-bottom: 0; } .lib-size-chart__table tbody th { font-weight: 600; } .lib-size-chart__table tr > :first-child { position: sticky; left: 0; z-index: 1; background: var(--lib-panel); border-right: 1px solid var(--lib-line); } .lib-size-chart__table thead tr > :first-child { background: var(--lib-head); } .lib-size-chart__page > :first-child { margin-top: 0; } .lib-size-chart__page > :not(table) { padding-inline: 1.25rem; } /* Measuring guide */ .lib-size-chart__guide { display: grid; gap: 1.5rem; margin-top: 1.75rem; align-items: start; } .lib-size-chart__media { border-radius: 12px; overflow: hidden; background: var(--lib-head); } .lib-size-chart__img { display: block; width: 100%; height: auto; } .lib-size-chart__notes { font-size: 0.9375rem; line-height: 1.65; } .lib-size-chart__notes > :first-child { margin-top: 0; } .lib-size-chart__notes > :last-child { margin-bottom: 0; } .lib-size-chart__notes h3, .lib-size-chart__notes h4 { margin: 1.25rem 0 0.25rem; font-size: 1rem; } @media (min-width: 990px) { .lib-size-chart[data-display='inline'] .lib-size-chart__guide:has(.lib-size-chart__media) { grid-template-columns: minmax(0, 360px) 1fr; gap: 2.5rem; } } /* Dialog: side drawer or centred modal. */ html:has(.lib-size-chart__dialog[open]) { overflow: hidden; } .lib-size-chart__dialog { box-sizing: border-box; width: min(100%, 36rem); max-width: 100%; max-height: 100%; padding: 0; border: 0; background: var(--lib-panel); color: var(--lib-text); box-shadow: 0 24px 60px -20px rgb(0 0 0 / 0.4); } .lib-size-chart__dialog::backdrop { background: rgb(12 10 9 / 0.55); } .lib-size-chart[data-display='drawer'] .lib-size-chart__dialog { height: 100%; margin: 0 0 0 auto; } .lib-size-chart[data-display='modal'] .lib-size-chart__dialog { width: min(100% - 2rem, 48rem); max-height: min(100% - 2rem, 52rem); border-radius: 16px; } .lib-size-chart__panel { padding: 0 clamp(1.25rem, 4vw, 2rem) 2rem; } .lib-size-chart__bar { position: sticky; top: 0; z-index: 2; display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin: 0 calc(-1 * clamp(1.25rem, 4vw, 2rem)) 1.25rem; padding: 1rem clamp(1.25rem, 4vw, 2rem); background: var(--lib-panel); border-bottom: 1px solid var(--lib-line); } .lib-size-chart__dialog .lib-size-chart__heading { font-size: clamp(1.25rem, 2vw, 1.5rem); } .lib-size-chart__dialog .lib-size-chart__text { margin: 0 0 1.25rem; } .lib-size-chart__close { flex: none; display: grid; place-items: center; width: 2.75rem; height: 2.75rem; padding: 0; border: 0; border-radius: 50%; background: var(--lib-head); color: inherit; cursor: pointer; } .lib-size-chart__close:hover { background: var(--lib-line); } .lib-size-chart__sr { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } /* Motion: the drawer slides in, the modal rises (never under reduced motion). */ @media (prefers-reduced-motion: no-preference) { .lib-size-chart[data-animate] .lib-size-chart__dialog[open] { animation: lib-size-chart-rise 0.28s cubic-bezier(0.2, 0.7, 0.2, 1); } .lib-size-chart[data-animate][data-display='drawer'] .lib-size-chart__dialog[open] { animation-name: lib-size-chart-slide; } .lib-size-chart[data-animate] .lib-size-chart__dialog[open]::backdrop { animation: lib-size-chart-fade 0.28s ease; } } @keyframes lib-size-chart-slide { from { transform: translateX(100%); } } @keyframes lib-size-chart-rise { from { opacity: 0; transform: translateY(16px); } } @keyframes lib-size-chart-fade { from { opacity: 0; } } {% endstylesheet %} {% javascript %} if (!customElements.get('lib-size-chart')) { customElements.define( 'lib-size-chart', class extends HTMLElement { connectedCallback() { this.dialog = this.querySelector('[data-dialog]'); this.opener = this.querySelector('[data-open]'); this.cleanup = []; this.setupDialog(); this.setupUnits(); } disconnectedCallback() { this.cleanup.forEach((fn) => fn()); this.cleanup = []; } on(target, type, handler) { target.addEventListener(type, handler); this.cleanup.push(() => target.removeEventListener(type, handler)); } setupDialog() { if (!this.dialog || !this.opener) return; // Browsers with button commands open and close the dialog on their own. const native = 'commandForElement' in HTMLButtonElement.prototype; if (!native) { this.on(this.opener, 'click', () => { if (!this.dialog.open) this.dialog.showModal(); }); const close = this.dialog.querySelector('[data-close]'); if (close) this.on(close, 'click', () => this.dialog.close()); } // A click on the backdrop (the dialog box itself, outside the panel) closes it. this.on(this.dialog, 'click', (e) => { if (e.target === this.dialog) this.dialog.close(); }); this.on(this.dialog, 'close', () => { if (!this.contains(document.activeElement) || document.activeElement === document.body) { this.opener.focus({ preventScroll: true }); } }); } // cm <-> in for numeric cells and ranges ("86", "86.5", "86-90"); other text is left alone. setupUnits() { const groups = this.querySelectorAll('[data-units]'); if (!groups.length) return; const base = this.dataset.baseUnit === 'in' ? 'in' : 'cm'; const cells = [...this.querySelectorAll('[data-measure], .lib-size-chart__page td')]; const pattern = /^\s*(\d+(?:[.,]\d+)?)(?:\s*([-–])\s*(\d+(?:[.,]\d+)?))?\s*$/; cells.forEach((cell) => { if (pattern.test(cell.textContent)) cell.dataset.original = cell.textContent.trim(); }); const convert = (value, unit) => { const n = parseFloat(value.replace(',', '.')); const out = unit === base ? n : base === 'cm' ? n / 2.54 : n * 2.54; return String(Math.round(out * 10) / 10); }; const apply = (unit) => { cells.forEach((cell) => { const original = cell.dataset.original; if (!original) return; if (unit === base) { cell.textContent = original; return; } const m = original.match(pattern); cell.textContent = m[3] ? `${convert(m[1], unit)}${m[2]}${convert(m[3], unit)}` : convert(m[1], unit); }); this.querySelectorAll('[data-unit]').forEach((btn) => { btn.setAttribute('aria-pressed', String(btn.dataset.unit === unit)); }); }; groups.forEach((group) => { group.hidden = false; group.querySelectorAll('[data-unit]').forEach((btn) => { this.on(btn, 'click', () => apply(btn.dataset.unit)); }); }); } } ); } {% endjavascript %} {% schema %} { "name": "Lib · Size chart", "tag": "section", "class": "lib-section", "settings": [ { "type": "header", "content": "Display" }, { "type": "select", "id": "display", "label": "Show the chart", "options": [ { "value": "drawer", "label": "In a side drawer, behind a button" }, { "value": "modal", "label": "In a pop-up, behind a button" }, { "value": "inline", "label": "On the page" } ], "default": "drawer" }, { "type": "text", "id": "trigger_label", "label": "Button label", "default": "Size chart", "visible_if": "{{ section.settings.display != 'inline' }}" }, { "type": "select", "id": "trigger_style", "label": "Button style", "options": [ { "value": "link", "label": "Text link with icon" }, { "value": "primary", "label": "Solid" }, { "value": "outline", "label": "Outline" } ], "default": "link", "visible_if": "{{ section.settings.display != 'inline' }}" }, { "type": "text", "id": "close_label", "label": "Close button (screen readers)", "default": "Close size chart", "visible_if": "{{ section.settings.display != 'inline' }}" }, { "type": "header", "content": "Heading" }, { "type": "inline_richtext", "id": "heading", "label": "Heading", "info": "In a drawer or pop-up the button label is used when this is empty.", "default": "Size chart" }, { "type": "select", "id": "heading_tag", "label": "Heading tag", "info": "Use H1 only when this is the page's main heading.", "options": [ { "value": "h1", "label": "H1" }, { "value": "h2", "label": "H2" }, { "value": "h3", "label": "H3" }, { "value": "p", "label": "Paragraph" } ], "default": "h2" }, { "type": "select", "id": "heading_size", "label": "Heading size", "options": [ { "value": "small", "label": "Small" }, { "value": "medium", "label": "Medium" }, { "value": "large", "label": "Large" }, { "value": "xlarge", "label": "Extra large" } ], "default": "medium" }, { "type": "richtext", "id": "text", "label": "Intro text" }, { "type": "header", "content": "Table" }, { "type": "select", "id": "source", "label": "Table source", "options": [ { "value": "table", "label": "Typed below" }, { "value": "page", "label": "A page" } ], "default": "table" }, { "type": "textarea", "id": "table", "label": "Table", "info": "One row per line, cells separated by |. The first line is the header, the first cell of each row the size.", "default": "Size | Chest | Waist | Length\nXS | 82-86 | 64-68 | 66\nS | 86-90 | 68-72 | 68\nM | 90-96 | 72-78 | 70\nL | 96-102 | 78-84 | 72\nXL | 102-108 | 84-90 | 74", "visible_if": "{{ section.settings.source == 'table' }}" }, { "type": "page", "id": "page", "label": "Page", "info": "Used when the table source is A page. Its content (usually a table) is shown as the chart." }, { "type": "text", "id": "caption", "label": "Table caption", "default": "Body measurements" }, { "type": "text", "id": "scroll_label", "label": "Table area name (screen readers)", "default": "Size chart table, scrolls sideways on small screens" }, { "type": "checkbox", "id": "unit_toggle", "label": "Show a cm / in switch", "info": "Converts number cells and ranges like 86-90. Other text stays as typed.", "default": true }, { "type": "select", "id": "base_unit", "label": "Values are typed in", "options": [ { "value": "cm", "label": "Centimetres" }, { "value": "in", "label": "Inches" } ], "default": "cm" }, { "type": "text", "id": "label_cm", "label": "Centimetres label", "default": "cm", "visible_if": "{{ section.settings.unit_toggle }}" }, { "type": "text", "id": "label_in", "label": "Inches label", "default": "in", "visible_if": "{{ section.settings.unit_toggle }}" }, { "type": "text", "id": "units_label", "label": "Switch name (screen readers)", "default": "Units", "visible_if": "{{ section.settings.unit_toggle }}" }, { "type": "header", "content": "How to measure" }, { "type": "richtext", "id": "notes", "label": "Measuring notes", "default": "

Chest: measure around the fullest part, keeping the tape level.

Waist: measure around your natural waistline.

Length: from the highest point of the shoulder to the hem.

" }, { "type": "image_picker", "id": "image", "label": "Desktop image" }, { "type": "image_picker", "id": "image_mobile", "label": "Mobile image", "info": "Optional. Shown below 750px." }, { "type": "header", "content": "Layout" }, { "type": "select", "id": "section_width", "label": "Width", "options": [ { "value": "page", "label": "Page width" }, { "value": "full", "label": "Full width" } ], "default": "page" }, { "type": "text_alignment", "id": "text_alignment", "label": "Heading and button alignment on desktop", "default": "left" }, { "type": "select", "id": "text_alignment_mobile", "label": "Heading and button alignment on mobile", "options": [ { "value": "left", "label": "Left" }, { "value": "center", "label": "Center" }, { "value": "right", "label": "Right" } ], "default": "left" }, { "type": "header", "content": "Colors" }, { "type": "color", "id": "color_bg", "label": "Background", "default": "#FFFFFF" }, { "type": "color_background", "id": "color_bg_gradient", "label": "Background gradient", "info": "Optional. Covers the background color." }, { "type": "color", "id": "color_text", "label": "Text", "default": "#231F1B" }, { "type": "color", "id": "color_accent", "label": "Accent (button, icon, unit switch)", "default": "#231F1B" }, { "type": "color", "id": "color_accent_text", "label": "Text on accent", "default": "#FFFFFF" }, { "type": "color", "id": "color_panel", "label": "Table and drawer background", "default": "#FFFFFF" }, { "type": "color", "id": "color_head", "label": "Table header background", "default": "#F4EFE8" }, { "type": "color", "id": "color_line", "label": "Lines", "default": "#E7E0D6" }, { "type": "header", "content": "Spacing and visibility" }, { "type": "range", "id": "padding_top", "label": "Top padding on desktop", "min": 0, "max": 200, "step": 4, "unit": "px", "default": 16 }, { "type": "range", "id": "padding_bottom", "label": "Bottom padding on desktop", "min": 0, "max": 200, "step": 4, "unit": "px", "default": 16 }, { "type": "range", "id": "padding_top_mobile", "label": "Top padding on mobile", "min": 0, "max": 160, "step": 4, "unit": "px", "default": 12 }, { "type": "range", "id": "padding_bottom_mobile", "label": "Bottom padding on mobile", "min": 0, "max": 160, "step": 4, "unit": "px", "default": 12 }, { "type": "select", "id": "show_on", "label": "Show on", "options": [ { "value": "all", "label": "All devices" }, { "value": "desktop", "label": "Desktop only" }, { "value": "mobile", "label": "Mobile only" } ], "default": "all" }, { "type": "text", "id": "anchor_id", "label": "Anchor ID", "info": "Optional. Link to this section with #your-id." }, { "type": "header", "content": "Motion" }, { "type": "checkbox", "id": "animate", "label": "Slide the drawer in", "info": "Never when reduced motion is on.", "default": true } ], "presets": [ { "name": "Lib · Size chart" } ] } {% endschema %}