# Size chart: free Shopify section

> A size table in a side drawer, a pop-up or on the page, with a cm / in switch, measuring notes and a how-to-measure image. Type the table or use a page.

Size chart is a free Shopify section that shows a size table behind a "Size chart" link or button, in a side drawer or a centred pop-up, or straight on the page. You type the table in the theme editor (one row per line, cells separated by |) or point it at a page, and shoppers can switch between centimetres and inches. It adds measuring notes and an optional how-to-measure image, and works in Online Store 2.0 themes without an app.

- **Category:** Product page
- **Use on:** Product, Collection, Any page
- **Reads:** Content only, or a page's content
- **Size:** 35 KB in 3 files, with a small script
- **License:** MIT, © 2026 Lintel
- **Page:** https://lintelagency.com/resources/shopify-sections/size-chart
- **Live demo:** https://lintelagency.com/resources/shopify-sections/demo/lib-size-chart.html
- **Added:** 2026-11-02

## What it’s for

- A "Size chart" link under the buy box on clothing, shoe or ring product templates
- A full size guide on a dedicated Sizing page, shown on the page with a how-to-measure photo
- One shared size table kept in a page and reused by several product templates

## Editor settings

In the theme editor the section is called “Lib · Size chart”. IDs, types, labels and defaults come from the section’s schema; the labels are what the theme editor shows.

### Display

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `display` | `select` | Show the chart | drawer | `drawer`, `modal`, `inline` |
| `trigger_label` | `text` | Button label | Size chart |  |
| `trigger_style` | `select` | Button style | link | `link`, `primary`, `outline` |
| `close_label` | `text` | Close button (screen readers) | Close size chart |  |

### Heading

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `heading` | `inline_richtext` | Heading | Size chart |  |
| `heading_tag` | `select` | Heading tag | h2 | `h1`, `h2`, `h3`, `p` |
| `heading_size` | `select` | Heading size | medium | `small`, `medium`, `large`, `xlarge` |
| `text` | `richtext` | Intro text | – |  |

### Table

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `source` | `select` | Table source | table | `table`, `page` |
| `table` | `textarea` | Table | Size \| Chest \| Waist \| Length XS \| 82-86 \| 64-68 \| 66 S \| 86-90 \| 68-72 \| 68 M \| 90-96 \| 72-78 \| 70 L \| 96-102 \| 78-84 \| 72 XL \| 102-108 \| 84-90 \| 74 |  |
| `page` | `page` | Page | – |  |
| `caption` | `text` | Table caption | Body measurements |  |
| `scroll_label` | `text` | Table area name (screen readers) | Size chart table, scrolls sideways on small screens |  |
| `unit_toggle` | `checkbox` | Show a cm / in switch | `true` |  |
| `base_unit` | `select` | Values are typed in | cm | `cm`, `in` |
| `label_cm` | `text` | Centimetres label | cm |  |
| `label_in` | `text` | Inches label | in |  |
| `units_label` | `text` | Switch name (screen readers) | Units |  |

### How to measure

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `notes` | `richtext` | Measuring notes | `<p><strong>Chest:</strong> measure around the fullest part, keeping the tape level.</p><p><strong>Waist:</strong> measure around your natural waistline.</p><p><strong>Length:</strong> from the highest point of the shoulder to the hem.</p>` |  |
| `image` | `image_picker` | Desktop image | – |  |
| `image_mobile` | `image_picker` | Mobile image | – |  |

### Layout

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `section_width` | `select` | Width | page | `page`, `full` |
| `text_alignment` | `text_alignment` | Heading and button alignment on desktop | left |  |
| `text_alignment_mobile` | `select` | Heading and button alignment on mobile | left | `left`, `center`, `right` |

### Colors

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `color_bg` | `color` | Background | #FFFFFF |  |
| `color_bg_gradient` | `color_background` | Background gradient | – |  |
| `color_text` | `color` | Text | #231F1B |  |
| `color_accent` | `color` | Accent (button, icon, unit switch) | #231F1B |  |
| `color_accent_text` | `color` | Text on accent | #FFFFFF |  |
| `color_panel` | `color` | Table and drawer background | #FFFFFF |  |
| `color_head` | `color` | Table header background | #F4EFE8 |  |
| `color_line` | `color` | Lines | #E7E0D6 |  |

### Spacing and visibility

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `padding_top` | `range` | Top padding on desktop | `16` | 0–200 px, step 4 |
| `padding_bottom` | `range` | Bottom padding on desktop | `16` | 0–200 px, step 4 |
| `padding_top_mobile` | `range` | Top padding on mobile | `12` | 0–160 px, step 4 |
| `padding_bottom_mobile` | `range` | Bottom padding on mobile | `12` | 0–160 px, step 4 |
| `show_on` | `select` | Show on | all | `all`, `desktop`, `mobile` |
| `anchor_id` | `text` | Anchor ID | – |  |

### Motion

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `animate` | `checkbox` | Slide the drawer in | `true` |  |

## Install

### In Shopify admin

1. Work on a copy, not your live theme: in Shopify admin go to **Online Store > Themes**, open the **…** menu on your current theme and choose **Duplicate**.
2. On the copy, open the **…** menu and choose **Edit code**.
3. In the `sections` folder, add a new file named `lib-size-chart.liquid` and paste the contents of `sections/lib-size-chart.liquid` from the source below. Save.
4. In the `snippets` folder, add `lib-image.liquid` and `lib-button.liquid` the same way. If a snippet is already there from another Lib section, it is the same file; keep one copy.
5. Open **Customize** on the copy, choose **Add section** and pick **Lib · Size chart**. Replace the sample content with your own and set the colors.
6. Preview the copy, then publish it when it looks right.

### With Shopify CLI

From your local theme folder, download the files and push only them to an unpublished theme (find its ID with `shopify theme list`):

```sh
curl -fsSL -o sections/lib-size-chart.liquid https://lintelagency.com/resources/shopify-sections/files/lib-size-chart.liquid
curl -fsSL -o snippets/lib-image.liquid https://lintelagency.com/resources/shopify-sections/files/lib-image.liquid
curl -fsSL -o snippets/lib-button.liquid https://lintelagency.com/resources/shopify-sections/files/lib-button.liquid
shopify theme push --theme <unpublished-theme-id> --only sections/lib-size-chart.liquid --only snippets/lib-image.liquid --only snippets/lib-button.liquid
```

Never push to the live theme: always pass the ID of an unpublished theme and never use `--live` or `--allow-live`. Then add the section in the theme editor as above.

## Files

- [sections/lib-size-chart.liquid](https://lintelagency.com/resources/shopify-sections/files/lib-size-chart.liquid): goes in `sections/`
- [snippets/lib-image.liquid](https://lintelagency.com/resources/shopify-sections/files/lib-image.liquid): goes in `snippets/`
- [snippets/lib-button.liquid](https://lintelagency.com/resources/shopify-sections/files/lib-button.liquid): goes in `snippets/`

## Questions

### How do I add my sizes?

Type them in the Table setting: one row per line, cells separated by a vertical bar (|). The first line is the header and the first cell of each row is the size, for example "S | 86-90 | 68-72". Or set the table source to a page and the page's content, usually a table made in the page editor, becomes the chart.

### Can shoppers switch between centimetres and inches?

Yes. Turn on the cm / in switch and say which unit you typed. Number cells and ranges such as 86-90 are converted to one decimal place; other text, like sizes or notes, stays as typed. Without JavaScript the switch is hidden and your typed values show.

### Is the drawer accessible?

It is a native dialog: focus moves into it, Escape and the close button close it, the rest of the page can't be reached while it is open, page scrolling is locked and focus returns to the button afterwards. The table has a caption and header cells, and on small screens it scrolls sideways inside a labelled region while the size column stays in view.

### Can I use different charts for different products?

Yes. Add the section to each product template that needs its own chart, or keep each chart in its own page and pick that page in the section, so editing the page updates every template that uses it.

### Can every label be translated?

Every word a shopper sees is a setting: the button label, heading, caption, unit labels and the screen-reader names for the close button, the unit switch and the table area. Change them in the theme editor or translate them with Shopify's Translate & Adapt.

## Source

### sections/lib-size-chart.liquid

```liquid
{%- 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 <dialog> 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: '<br />'
  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 -%}
  <div class="lib-size-chart__body">
    {%- if s.unit_toggle -%}
      <div class="lib-size-chart__units" role="group" aria-label="{{ s.units_label | escape }}" data-units hidden>
        <button type="button" class="lib-size-chart__unit" data-unit="cm" aria-pressed="{% if s.base_unit == 'cm' %}true{% else %}false{% endif %}">
          {{- s.label_cm | escape -}}
        </button>
        <button type="button" class="lib-size-chart__unit" data-unit="in" aria-pressed="{% if s.base_unit == 'in' %}true{% else %}false{% endif %}">
          {{- s.label_in | escape -}}
        </button>
      </div>
    {%- endif -%}

    {%- if rows != blank -%}
      <div class="lib-size-chart__scroll" role="region" tabindex="0" aria-label="{{ s.scroll_label | escape }}">
        <table class="lib-size-chart__table">
          {%- if s.caption != blank -%}
            <caption class="lib-size-chart__caption">{{ s.caption | escape }}</caption>
          {%- endif -%}
          {%- liquid
            assign header = ''
            for row in rows
              assign line = row | strip
              if line != blank
                assign header = line
                break
              endif
            endfor
          -%}
          <thead>
            <tr>
              {%- assign cells = header | split: '|' -%}
              {%- for cell in cells -%}
                <th scope="col">{{ cell | strip | escape }}</th>
              {%- endfor -%}
            </tr>
          </thead>
          <tbody>
            {%- 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: '|' -%}
              <tr>
                {%- for cell in cells -%}
                  {%- if forloop.first -%}
                    <th scope="row">{{ cell | strip | escape }}</th>
                  {%- else -%}
                    <td data-measure>{{ cell | strip | escape }}</td>
                  {%- endif -%}
                {%- endfor -%}
              </tr>
            {%- endfor -%}
          </tbody>
        </table>
      </div>
    {%- elsif has_page -%}
      <div class="lib-size-chart__scroll lib-size-chart__page" role="region" tabindex="0" aria-label="{{ s.scroll_label | escape }}">
        {{ s.page.content }}
      </div>
    {%- endif -%}

    {%- if s.notes != blank or s.image != blank or s.image_mobile != blank -%}
      <div class="lib-size-chart__guide">
        {%- if s.image != blank or s.image_mobile != blank -%}
          <div class="lib-size-chart__media">
            {%- render 'lib-image',
              image: s.image,
              image_mobile: s.image_mobile,
              class: 'lib-size-chart__img',
              sizes: '(min-width: 990px) 360px, 100vw',
              eager: eager
            -%}
          </div>
        {%- endif -%}
        {%- if s.notes != blank -%}
          <div class="lib-size-chart__notes">{{ s.notes }}</div>
        {%- endif -%}
      </div>
    {%- endif -%}
  </div>
{%- endcapture -%}

<lib-size-chart
  class="lib-size-chart lib-size-chart--{{ s.section_width }}"
  {% if s.anchor_id != blank %}
    id="{{ s.anchor_id | handleize }}"
  {% endif %}
  data-show="{{ s.show_on }}"
  data-display="{{ display }}"
  data-base-unit="{{ s.base_unit }}"
  {% if s.animate %}
    data-animate
  {% endif %}
  style="
    --lib-bg: {{ s.color_bg }};
    {% if s.color_bg_gradient != blank %}--lib-bg-image: {{ s.color_bg_gradient }};{% endif %}
    --lib-text: {{ s.color_text }};
    --lib-accent: {{ s.color_accent }};
    --lib-accent-text: {{ s.color_accent_text }};
    --lib-panel: {{ s.color_panel }};
    --lib-head: {{ s.color_head }};
    --lib-line: {{ s.color_line }};
    --lib-pt: {{ s.padding_top }}px;
    --lib-pb: {{ s.padding_bottom }}px;
    --lib-pt-m: {{ s.padding_top_mobile }}px;
    --lib-pb-m: {{ s.padding_bottom_mobile }}px;
  "
>
  <div class="lib-size-chart__inner" data-align="{{ s.text_alignment }}" data-align-m="{{ s.text_alignment_mobile }}">
    {%- if display == 'inline' -%}
      {%- if s.heading != blank -%}
        <{{ heading_tag }} class="lib-size-chart__heading lib-size-chart__heading--{{ s.heading_size }}">
          {{- s.heading -}}
        </{{ heading_tag }}>
      {%- endif -%}
      {%- if s.text != blank -%}
        <div class="lib-size-chart__text">{{ s.text }}</div>
      {%- endif -%}
      {{ chart }}
    {%- else -%}
      <div class="lib-size-chart__trigger-row">
        {%- if s.trigger_style == 'link' -%}
          <button
            type="button"
            class="lib-size-chart__link"
            aria-haspopup="dialog"
            aria-controls="{{ uid }}"
            command="show-modal"
            commandfor="{{ uid }}"
            data-open
          >
            <svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8h18v8H3z"/><path d="M7 8v3M11 8v4M15 8v3M19 8v4"/></svg>
            <span>{{ s.trigger_label | escape }}</span>
          </button>
        {%- else -%}
          <button
            type="button"
            class="lib-btn lib-btn--{{ s.trigger_style }} lib-size-chart__button"
            aria-haspopup="dialog"
            aria-controls="{{ uid }}"
            command="show-modal"
            commandfor="{{ uid }}"
            data-open
          >
            <span class="lib-btn__label">{{ s.trigger_label | escape }}</span>
          </button>
          {%- 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 -%}
      </div>

      <dialog class="lib-size-chart__dialog" id="{{ uid }}" aria-labelledby="{{ heading_id }}" closedby="any" data-dialog>
        <div class="lib-size-chart__panel">
          <div class="lib-size-chart__bar">
            <{{ heading_tag }} id="{{ heading_id }}" class="lib-size-chart__heading lib-size-chart__heading--{{ s.heading_size }}">
              {{- heading_text -}}
            </{{ heading_tag }}>
            <button type="button" class="lib-size-chart__close" command="close" commandfor="{{ uid }}" data-close>
              <svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false"><path d="M6 6l12 12M18 6 6 18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>
              <span class="lib-size-chart__sr">{{ s.close_label | escape }}</span>
            </button>
          </div>
          {%- if s.text != blank -%}
            <div class="lib-size-chart__text">{{ s.text }}</div>
          {%- endif -%}
          {{ chart }}
        </div>
      </dialog>
    {%- endif -%}
  </div>
</lib-size-chart>

{% 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": "<p><strong>Chest:</strong> measure around the fullest part, keeping the tape level.</p><p><strong>Waist:</strong> measure around your natural waistline.</p><p><strong>Length:</strong> from the highest point of the shoulder to the hem.</p>" },
    { "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 %}
```

### snippets/lib-image.liquid

```liquid
{%- comment -%}
  MIT License · (c) 2026 Lintel · https://lintelagency.com/resources/shopify-sections
{%- endcomment -%}
{% doc %}
  Responsive image with optional art direction: a separate mobile image (below 750px),
  each honouring its own focal point from the theme editor. Falls back to a placeholder.

  @param {image} [image] - Desktop image (also used on mobile when no mobile image is set)
  @param {image} [image_mobile] - Mobile image, below 750px
  @param {string} [class] - Class for the img element
  @param {string} [sizes] - sizes attribute for the desktop image (default 100vw)
  @param {boolean} [eager] - Load immediately with high fetch priority (use for the first sections on a page)
  @param {string} [placeholder] - placeholder_svg_tag name when no image is set (default 'lifestyle-1')

  @example
  {% render 'lib-image', image: section.settings.image, image_mobile: section.settings.image_mobile, class: 'lib-hero__img', sizes: '(min-width: 990px) 50vw, 100vw', eager: true %}
{% enddoc %}

{%- liquid
  assign desktop = image
  if desktop == blank
    assign desktop = image_mobile
  endif
  assign img_sizes = sizes | default: '100vw'
  assign img_loading = 'lazy'
  assign img_priority = 'auto'
  if eager
    assign img_loading = 'eager'
    assign img_priority = 'high'
  endif
  assign art = false
  if image != blank and image_mobile != blank
    assign art = true
  endif
-%}

{%- if desktop != blank -%}
  <picture
    class="lib-picture{% if art %} lib-picture--art{% endif %}"
    {% if art %}
      style="--lib-pos-m: {{ image_mobile.presentation.focal_point | default: '50% 50%' }};"
    {% endif %}
  >
    {%- if art -%}
      <source
        media="(max-width: 749px)"
        srcset="
          {{ image_mobile | image_url: width: 375 }} 375w,
          {{ image_mobile | image_url: width: 750 }} 750w,
          {{ image_mobile | image_url: width: 1100 }} 1100w,
          {{ image_mobile | image_url: width: 1500 }} 1500w
        "
        sizes="100vw"
        width="{{ image_mobile.width }}"
        height="{{ image_mobile.height }}"
      >
    {%- endif -%}
    {{
      desktop
      | image_url: width: 2400
      | image_tag:
        class: class,
        loading: img_loading,
        fetchpriority: img_priority,
        sizes: img_sizes,
        widths: '600, 900, 1200, 1600, 2000, 2400'
    }}
  </picture>
{%- else -%}
  {{ placeholder | default: 'lifestyle-1' | placeholder_svg_tag: class }}
{%- endif -%}

{% stylesheet %}
  .lib-picture { display: contents; }
  @media (max-width: 749px) {
    /* image_tag sets the desktop focal point inline; the mobile image needs its own. */
    .lib-picture--art img { object-position: var(--lib-pos-m) !important; }
  }
{% endstylesheet %}
```

### snippets/lib-button.liquid

```liquid
{%- comment -%}
  MIT License · (c) 2026 Lintel · https://lintelagency.com/resources/shopify-sections
{%- endcomment -%}
{% doc %}
  Animated call-to-action link. Light sweep on hover, arrow slides right.
  Colors come from the parent section's --lib-accent / --lib-accent-text / --lib-text vars.
  Inspired by Uiverse.io button patterns; rewritten for themes, no dependencies.

  @param {string} label - Button text
  @param {string} [url] - Link target; renders a disabled-looking span when blank
  @param {string} [style] - 'primary' (default), 'outline' or 'text'
  @param {boolean} [new_tab] - Open the link in a new tab
  @param {boolean} [arrow] - Show the trailing arrow (default true)

  @example
  {% render 'lib-button', label: section.settings.button_label, url: section.settings.button_url, style: 'outline' %}
{% enddoc %}

{%- liquid
  assign btn_style = style | default: 'primary'
  assign show_arrow = true
  if arrow == false
    assign show_arrow = false
  endif
-%}

{%- unless label == blank -%}
{%- capture inner -%}
  <span class="lib-btn__label">{{ label | escape }}</span>
  {%- if show_arrow -%}
    <svg class="lib-btn__arrow" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false">
      <path d="M3 8h9M8.5 4.5 12 8l-3.5 3.5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
    </svg>
  {%- endif -%}
{%- endcapture -%}

{%- if url != blank -%}
  <a
    class="lib-btn lib-btn--{{ btn_style }}"
    href="{{ url }}"
    {% if new_tab %}
      target="_blank" rel="noopener"
    {% endif %}
  >
    {{- inner -}}
  </a>
{%- else -%}
  <span class="lib-btn lib-btn--{{ btn_style }}" aria-disabled="true">{{ inner }}</span>
{%- endif -%}
{%- endunless -%}

{% stylesheet %}
  .lib-btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 0.5em;
    padding: 0.85em 1.5em;
    border-radius: var(--lib-radius, 999px);
    font: inherit;
    font-weight: 600;
    line-height: 1;
    text-decoration: none;
    overflow: hidden;
    isolation: isolate;
    transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
  }
  .lib-btn--primary {
    background: var(--lib-accent, #111);
    color: var(--lib-accent-text, #fff);
    box-shadow: 0 1px 2px rgb(0 0 0 / 0.12);
  }
  .lib-btn--outline {
    background: transparent;
    color: var(--lib-text, #111);
    box-shadow: inset 0 0 0 1.5px currentColor;
  }
  .lib-btn--text {
    padding-inline: 0;
    background: none;
    color: var(--lib-text, #111);
    text-decoration: underline;
    text-underline-offset: 0.3em;
    overflow: visible;
  }
  .lib-btn--text::before { display: none; }
  .lib-btn--text:hover { transform: none; box-shadow: none; }
  .lib-btn::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    background: linear-gradient(110deg, transparent 30%, rgb(255 255 255 / 0.35) 50%, transparent 70%);
    transform: translateX(-120%);
    transition: transform 0.6s ease;
  }
  .lib-btn--outline::before {
    background: linear-gradient(110deg, transparent 30%, rgb(127 127 127 / 0.18) 50%, transparent 70%);
  }
  .lib-btn:hover {
    transform: translateY(-2px);
    box-shadow: 0 8px 20px -8px rgb(0 0 0 / 0.35);
  }
  .lib-btn:hover::before { transform: translateX(120%); }
  .lib-btn__arrow { transition: transform 0.2s ease; }
  .lib-btn:hover .lib-btn__arrow { transform: translateX(3px); }
  .lib-btn:focus-visible {
    outline: 2px solid var(--lib-accent, #111);
    outline-offset: 3px;
  }
  .lib-btn[aria-disabled='true'] { pointer-events: none; }
  @media (prefers-reduced-motion: reduce) {
    .lib-btn, .lib-btn::before, .lib-btn__arrow { transition: none; }
    .lib-btn:hover { transform: none; }
  }
{% endstylesheet %}
```

## License

MIT License, © 2026 Lintel. Free to use, change and ship in your own store or in client work; keep the license line at the top of each file.
