# Collection tabs: free Shopify section

> A product grid with tabs: New in, Best sellers, Sale or any collections in one section, each with a View all link. Accessible tabs, works without JavaScript.

Collection tabs is a free Shopify section that puts several collections in one product grid with tabs, such as New in, Best sellers and Sale. Each tab is a block with a collection picker and an optional label, shows up to 16 products as cards with price, badges and an optional quick add, and can end with a View all link to its collection. All products are rendered by Liquid, so search engines and shoppers without JavaScript see every group in turn; a small script turns them into keyboard-accessible tabs. It works in Online Store 2.0 themes without an app.

- **Category:** Product and collection discovery
- **Use on:** Home, Landing pages, Collection
- **Reads:** Collections and their products
- **Size:** 39 KB in 4 files, with a small script
- **License:** MIT, © 2026 Lintel
- **Page:** https://lintelagency.com/resources/shopify-sections/collection-tabs
- **Live demo:** https://lintelagency.com/resources/shopify-sections/demo/lib-collection-tabs.html
- **Added:** 2026-11-02

## What it’s for

- A home page block with New in, Best sellers and Sale, instead of three featured collection sections
- A gift guide landing page with one tab per budget or recipient
- A collection page with tabs for sub-ranges (Mugs, Plates, Bowls)

## Editor settings

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

### Content

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `heading` | `inline_richtext` | Heading | Shop the range |  |
| `heading_tag` | `select` | Heading tag | h2 | `h1`, `h2`, `h3`, `p` |
| `heading_size` | `select` | Heading size | medium | `small`, `medium`, `large` |
| `text` | `richtext` | Intro text | – |  |
| `tabs_label` | `text` | Tab list name (screen readers) | Collections |  |
| `tab_style` | `select` | Tab style | pill | `pill`, `underline` |
| `view_all_label` | `text` | View all link | View all |  |
| `view_all_style` | `select` | View all style | outline | `primary`, `outline`, `text` |

### Products

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `products_per_tab` | `range` | Products per tab | `4` | 2–16, step 1 |
| `image_ratio` | `select` | Image shape | portrait | `square`, `portrait`, `tall`, `landscape` |
| `card_style` | `select` | Card style | plain | `plain`, `card` |
| `card_radius` | `range` | Corner radius | `12` | 0–24 px, step 2 |
| `show_vendor` | `checkbox` | Show vendor | `false` |  |
| `show_second_image` | `checkbox` | Show second image on hover | `true` |  |
| `show_quick_add` | `checkbox` | Show quick add | `false` |  |
| `quick_add_label` | `text` | Quick add button | Add to cart |  |
| `choose_label` | `text` | Link for products with options | Choose options |  |
| `sale_label` | `text` | Sale badge | Sale |  |
| `sold_out_label` | `text` | Sold out badge | Sold out |  |
| `sale_price_label` | `text` | Sale price label (screen readers) | Sale price |  |
| `regular_price_label` | `text` | Regular price label (screen readers) | Regular price |  |

### Layout

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `columns` | `range` | Columns on desktop | `4` | 2–6, step 1 |
| `columns_mobile` | `range` | Columns on mobile | `2` | 1–2, step 1 |
| `layout_mobile` | `select` | Layout on mobile | grid | `grid`, `row` |
| `section_width` | `select` | Width | page | `page`, `full` |
| `text_alignment` | `select` | Heading and tabs alignment on desktop | center | `left`, `center` |
| `text_alignment_mobile` | `select` | Heading and tabs alignment on mobile | left | `left`, `center` |

### Colors

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `color_bg` | `color` | Background | #FFFFFF |  |
| `color_bg_gradient` | `color_background` | Background gradient | – |  |
| `color_text` | `color` | Text | #1F1B17 |  |
| `color_accent` | `color` | Accent (buttons, badges) | #7A5A3A |  |
| `color_accent_text` | `color` | Text on accent | #FFFFFF |  |
| `color_card` | `color` | Card background | #FAF6F1 |  |

### Spacing and visibility

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `padding_top` | `range` | Top padding on desktop | `64` | 0–200 px, step 4 |
| `padding_bottom` | `range` | Bottom padding on desktop | `64` | 0–200 px, step 4 |
| `padding_top_mobile` | `range` | Top padding on mobile | `40` | 0–160 px, step 4 |
| `padding_bottom_mobile` | `range` | Bottom padding on mobile | `40` | 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` | Fade products in when switching tabs | `true` |  |

## Blocks

Up to 6 blocks in total.

### Tab (`tab`)

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `collection` | `collection` | Collection | – |  |
| `label` | `text` | Tab label | – |  |

## 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-collection-tabs.liquid` and paste the contents of `sections/lib-collection-tabs.liquid` from the source below. Save.
4. In the `snippets` folder, add `lib-product-card.liquid` and `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 · Collection tabs**. 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-collection-tabs.liquid https://lintelagency.com/resources/shopify-sections/files/lib-collection-tabs.liquid
curl -fsSL -o snippets/lib-product-card.liquid https://lintelagency.com/resources/shopify-sections/files/lib-product-card.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-collection-tabs.liquid --only snippets/lib-product-card.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-collection-tabs.liquid](https://lintelagency.com/resources/shopify-sections/files/lib-collection-tabs.liquid): goes in `sections/`
- [snippets/lib-product-card.liquid](https://lintelagency.com/resources/shopify-sections/files/lib-product-card.liquid): goes in `snippets/`
- [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 many tabs can I add?

Up to six. Each tab is a block with a collection; its label is the collection's title unless you type your own. With one tab the section shows a plain product grid without a tab list.

### Are the tabs accessible?

Yes. They follow the WAI-ARIA tabs pattern: real buttons in a tab list, the arrow keys, Home and End move between tabs, the selected tab is announced, and each panel is labelled by its tab. Focus is always visible.

### Do search engines see the products in hidden tabs?

Yes. Every tab's products are in the page's HTML; the script only hides the panels that aren't selected. Without JavaScript the groups show one after another with their own titles.

### Can shoppers add to cart from the grid?

Turn on quick add: single-variant products get an add to cart button (Shopify's own product form), products with options link to their page with "Choose options".

## Source

### sections/lib-collection-tabs.liquid

```liquid
{%- comment -%}
  MIT License · (c) 2026 Lintel · https://lintelagency.com/resources/shopify-sections
{%- endcomment -%}
{%- comment -%}
  Collection tabs: a product grid with tabs, one tab per collection (New in, Best sellers, Sale…),
  in a single section. Every tab's products are rendered by Liquid, so search engines and shoppers
  without JavaScript see all of them, one titled group after another; a small custom element turns
  the groups into tabs (WAI-ARIA tabs pattern: arrow keys, Home, End). Each tab can end with a
  "view all" link to its collection. Cards show price, badges and an optional quick add.
  Depends on: snippets/lib-product-card.liquid, snippets/lib-image.liquid, snippets/lib-button.liquid
{%- endcomment -%}

{%- liquid
  assign s = section.settings
  assign heading_tag = s.heading_tag | default: 'h2'
  assign panel_tag = 'h3'
  assign card_tag = 'h3'
  if heading_tag == 'h3' or heading_tag == 'p'
    assign panel_tag = 'h4'
    assign card_tag = 'h4'
  endif
  assign cols = s.columns | plus: 0
  assign cols_m = s.columns_mobile | plus: 0
  assign limit = s.products_per_tab | plus: 0
  assign img_vw = 100 | divided_by: cols
  assign img_sizes = '(min-width: 990px) ' | append: img_vw | append: 'vw, (min-width: 750px) 33vw, 50vw'
-%}

<lib-collection-tabs
  class="lib-tabs lib-tabs--{{ s.section_width }}"
  {% if s.anchor_id != blank %}
    id="{{ s.anchor_id | handleize }}"
  {% endif %}
  data-show="{{ s.show_on }}"
  data-card="{{ s.card_style }}"
  data-layout-m="{{ s.layout_mobile }}"
  data-tab-style="{{ s.tab_style }}"
  {% 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-card: {{ s.color_card }};
    --lib-cols: {{ cols }};
    --lib-cols-m: {{ cols_m }};
    --lib-radius-card: {{ s.card_radius }}px;
    --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-tabs__inner">
    <header class="lib-tabs__header" data-align="{{ s.text_alignment }}" data-align-m="{{ s.text_alignment_mobile }}">
      {%- if s.heading != blank -%}
        <{{ heading_tag }} class="lib-tabs__heading lib-tabs__heading--{{ s.heading_size }}">
          {{- s.heading -}}
        </{{ heading_tag }}>
      {%- endif -%}
      {%- if s.text != blank -%}
        <div class="lib-tabs__text">{{ s.text }}</div>
      {%- endif -%}

      {%- if section.blocks.size > 1 -%}
        {%- comment -%} The script shows the tab list; without it every group shows with its own title. {%- endcomment -%}
        <div class="lib-tabs__tablist" role="tablist" aria-label="{{ s.tabs_label | escape }}" hidden data-tablist>
          {%- for block in section.blocks -%}
            {%- assign label = block.settings.label | default: block.settings.collection.title | default: 'Collection' -%}
            <button
              type="button"
              role="tab"
              class="lib-tabs__tab"
              id="LibTab-{{ block.id }}"
              aria-controls="LibPanel-{{ block.id }}"
              aria-selected="{% if forloop.first %}true{% else %}false{% endif %}"
              {% unless forloop.first %}
                tabindex="-1"
              {% endunless %}
              {{ block.shopify_attributes }}
            >
              {{- label | escape -}}
            </button>
          {%- endfor -%}
        </div>
      {%- endif -%}
    </header>

    {%- for block in section.blocks -%}
      {%- liquid
        assign b = block.settings
        assign c = b.collection
        assign label = b.label | default: c.title | default: 'Collection'
        assign id_prefix = 'LibTabs-' | append: block.id
        assign first_tab = forloop.first
      -%}
      <div class="lib-tabs__panel" id="LibPanel-{{ block.id }}" data-panel data-tab="LibTab-{{ block.id }}">
        <{{ panel_tag }} class="lib-tabs__panel-title">{{ label | escape }}</{{ panel_tag }}>
        <ul class="lib-tabs__grid" role="list">
          {%- if c != blank and c.products.size > 0 -%}
            {%- for item in c.products limit: limit -%}
              {%- liquid
                assign eager = false
                if section.index <= 2 and first_tab and forloop.index <= cols
                  assign eager = true
                endif
              -%}
              <li class="lib-tabs__item">
                {%- render 'lib-product-card',
                  product: item,
                  title_tag: card_tag,
                  ratio: s.image_ratio,
                  sizes: img_sizes,
                  eager: eager,
                  show_vendor: s.show_vendor,
                  show_second_image: s.show_second_image,
                  show_quick_add: s.show_quick_add,
                  quick_add_label: s.quick_add_label,
                  choose_label: s.choose_label,
                  sold_out_label: s.sold_out_label,
                  sale_label: s.sale_label,
                  sale_price_label: s.sale_price_label,
                  regular_price_label: s.regular_price_label,
                  id_prefix: id_prefix
                -%}
              </li>
            {%- endfor -%}
          {%- elsif request.design_mode -%}
            {%- for i in (1..cols) -%}
              <li class="lib-tabs__item">{%- render 'lib-product-card', title_tag: card_tag, ratio: s.image_ratio -%}</li>
            {%- endfor -%}
          {%- endif -%}
        </ul>
        {%- if s.view_all_label != blank and c != blank -%}
          <div class="lib-tabs__more">
            {%- render 'lib-button', label: s.view_all_label, url: c.url, style: s.view_all_style -%}
          </div>
        {%- endif -%}
      </div>
    {%- endfor -%}
  </div>
</lib-collection-tabs>

{% stylesheet %}
  .lib-tabs {
    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-tabs__inner { max-width: 1200px; margin: 0 auto; }
  .lib-tabs--full .lib-tabs__inner { max-width: none; }
  @media (max-width: 749px) {
    .lib-tabs { padding-top: var(--lib-pt-m); padding-bottom: var(--lib-pb-m); }
    .lib-tabs[data-show='desktop'] { display: none; }
  }
  @media (min-width: 750px) {
    .lib-tabs[data-show='mobile'] { display: none; }
  }

  .lib-tabs__header { display: grid; gap: 0.75rem; margin-bottom: clamp(1.25rem, 3vw, 2rem); }
  .lib-tabs__heading {
    margin: 0;
    font-size: clamp(1.75rem, 3vw, 2.5rem);
    line-height: 1.15;
    letter-spacing: -0.02em;
    text-wrap: balance;
  }
  .lib-tabs__heading--small { font-size: clamp(1.25rem, 1.8vw, 1.5rem); }
  .lib-tabs__heading--large { font-size: clamp(2rem, 4vw, 3.25rem); }
  .lib-tabs__text { max-width: 40rem; font-size: 1rem; line-height: 1.6; opacity: 0.85; }
  .lib-tabs__text > :first-child { margin-top: 0; }
  .lib-tabs__text > :last-child { margin-bottom: 0; }
  .lib-tabs__header[data-align='center'] { justify-items: center; text-align: center; }
  @media (max-width: 749px) {
    .lib-tabs__header[data-align-m='left'] { justify-items: start; text-align: left; }
    .lib-tabs__header[data-align-m='center'] { justify-items: center; text-align: center; }
  }

  /* Tabs: a row that scrolls sideways when the labels don't fit. */
  .lib-tabs__tablist {
    display: flex;
    gap: 0.5rem;
    max-width: 100%;
    margin-top: 0.5rem;
    padding: 0.25rem;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .lib-tabs__tablist[hidden] { display: none; }
  .lib-tabs__tab {
    flex: none;
    min-height: 2.75rem;
    padding: 0.55rem 1.1rem;
    border: 1.5px solid color-mix(in srgb, var(--lib-text) 22%, transparent);
    border-radius: 999px;
    background: transparent;
    color: inherit;
    font: inherit;
    font-size: 0.9375rem;
    font-weight: 600;
    line-height: 1.2;
    white-space: nowrap;
    cursor: pointer;
    transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
  }
  .lib-tabs__tab:hover { border-color: var(--lib-text); }
  .lib-tabs__tab[aria-selected='true'] { background: var(--lib-text); border-color: var(--lib-text); color: var(--lib-bg); }
  .lib-tabs__tab:focus-visible { outline: 2px solid var(--lib-accent); outline-offset: 2px; }
  .lib-tabs[data-tab-style='underline'] .lib-tabs__tablist { gap: 1.5rem; padding-inline: 0; }
  .lib-tabs[data-tab-style='underline'] .lib-tabs__tab {
    padding-inline: 0;
    border: 0;
    border-bottom: 2px solid transparent;
    border-radius: 0;
    opacity: 0.7;
  }
  .lib-tabs[data-tab-style='underline'] .lib-tabs__tab[aria-selected='true'] {
    background: none;
    color: inherit;
    border-bottom-color: currentColor;
    opacity: 1;
  }

  .lib-tabs__panel + .lib-tabs__panel { margin-top: clamp(2rem, 5vw, 3.5rem); }
  .lib-tabs[data-ready] .lib-tabs__panel + .lib-tabs__panel { margin-top: 0; }
  .lib-tabs__panel[hidden] { display: none; }
  .lib-tabs__panel:focus-visible { outline: 2px solid var(--lib-accent); outline-offset: 6px; border-radius: 4px; }
  .lib-tabs__panel-title { margin: 0 0 1rem; font-size: 1.25rem; line-height: 1.3; }
  .lib-tabs[data-ready] .lib-tabs__panel-title {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .lib-tabs__grid {
    display: grid;
    grid-template-columns: repeat(var(--lib-cols), minmax(0, 1fr));
    gap: clamp(1rem, 2vw, 1.5rem);
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .lib-tabs__item { display: flex; min-width: 0; }
  @media (max-width: 749px) {
    .lib-tabs__grid { grid-template-columns: repeat(var(--lib-cols-m), minmax(0, 1fr)); }
    .lib-tabs[data-layout-m='row'] .lib-tabs__grid {
      grid-auto-flow: column;
      grid-template-columns: none;
      grid-auto-columns: calc((100% - 1rem) / var(--lib-cols-m) - 2.5rem);
      overflow-x: auto;
      scroll-snap-type: x mandatory;
      scrollbar-width: none;
      padding-bottom: 0.25rem;
    }
    .lib-tabs[data-layout-m='row'] .lib-tabs__item { scroll-snap-align: start; }
  }
  .lib-tabs__more { display: flex; justify-content: center; margin-top: clamp(1.5rem, 3vw, 2.25rem); }

  @media (prefers-reduced-motion: no-preference) {
    .lib-tabs[data-animate] .lib-tabs__panel[data-shown] .lib-tabs__item { animation: lib-tabs-in 0.45s cubic-bezier(0.2, 0.7, 0.2, 1) both; }
    .lib-tabs[data-animate] .lib-tabs__panel[data-shown] .lib-tabs__item:nth-child(2) { animation-delay: 40ms; }
    .lib-tabs[data-animate] .lib-tabs__panel[data-shown] .lib-tabs__item:nth-child(3) { animation-delay: 80ms; }
    .lib-tabs[data-animate] .lib-tabs__panel[data-shown] .lib-tabs__item:nth-child(n + 4) { animation-delay: 120ms; }
  }
  @media (prefers-reduced-motion: reduce) {
    .lib-tabs__tab { transition: none; }
  }
  @keyframes lib-tabs-in {
    from { opacity: 0; transform: translateY(10px); }
  }
{% endstylesheet %}

{% javascript %}
  (() => {
    if (customElements.get('lib-collection-tabs')) return;

    class LibCollectionTabs extends HTMLElement {
      connectedCallback() {
        this.tablist = this.querySelector('[data-tablist]');
        if (!this.tablist) return;
        this.tabs = [...this.tablist.querySelectorAll('[role="tab"]')];
        this.panels = this.tabs.map((tab) => this.querySelector(`#${CSS.escape(tab.getAttribute('aria-controls'))}`));
        this.panels.forEach((panel, i) => {
          panel.setAttribute('role', 'tabpanel');
          panel.setAttribute('aria-labelledby', this.tabs[i].id);
          panel.tabIndex = 0;
        });
        this.tablist.hidden = false;
        this.toggleAttribute('data-ready', true);
        this.select(Math.max(0, this.tabs.findIndex((t) => t.getAttribute('aria-selected') === 'true')), false);

        this.tablist.addEventListener('click', (event) => {
          const tab = event.target.closest('[role="tab"]');
          if (tab) this.select(this.tabs.indexOf(tab), true);
        });
        this.tablist.addEventListener('keydown', (event) => this.onKey(event));
        // Theme editor: selecting a tab's block shows its panel.
        this.addEventListener('shopify:block:select', (event) => {
          const index = this.tabs.findIndex((t) => t.id === `LibTab-${event.detail.blockId}`);
          if (index >= 0) this.select(index, false);
        });
      }

      onKey(event) {
        const current = this.tabs.indexOf(document.activeElement);
        if (current < 0) return;
        const last = this.tabs.length - 1;
        const moves = {ArrowRight: current + 1, ArrowLeft: current - 1, Home: 0, End: last};
        if (!(event.key in moves)) return;
        event.preventDefault();
        const next = (moves[event.key] + this.tabs.length) % this.tabs.length;
        this.select(next, true);
      }

      select(index, focus) {
        this.tabs.forEach((tab, i) => {
          const active = i === index;
          tab.setAttribute('aria-selected', String(active));
          tab.tabIndex = active ? 0 : -1;
          this.panels[i].hidden = !active;
          this.panels[i].toggleAttribute('data-shown', active && focus);
        });
        if (focus) {
          this.tabs[index].focus();
          this.tabs[index].scrollIntoView({block: 'nearest', inline: 'nearest'});
        }
      }
    }
    customElements.define('lib-collection-tabs', LibCollectionTabs);
  })();
{% endjavascript %}

{% schema %}
{
  "name": "Lib · Collection tabs",
  "tag": "section",
  "class": "lib-section",
  "max_blocks": 6,
  "settings": [
    { "type": "header", "content": "Content" },
    { "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "Shop the range" },
    {
      "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" }
      ],
      "default": "medium"
    },
    { "type": "richtext", "id": "text", "label": "Intro text" },
    { "type": "text", "id": "tabs_label", "label": "Tab list name (screen readers)", "default": "Collections" },
    {
      "type": "select",
      "id": "tab_style",
      "label": "Tab style",
      "options": [
        { "value": "pill", "label": "Pills" },
        { "value": "underline", "label": "Underline" }
      ],
      "default": "pill"
    },
    { "type": "text", "id": "view_all_label", "label": "View all link", "info": "Shown under each tab, linking to its collection. Leave empty to hide.", "default": "View all" },
    {
      "type": "select",
      "id": "view_all_style",
      "label": "View all style",
      "options": [
        { "value": "primary", "label": "Solid" },
        { "value": "outline", "label": "Outline" },
        { "value": "text", "label": "Text link" }
      ],
      "default": "outline"
    },

    { "type": "header", "content": "Products" },
    { "type": "range", "id": "products_per_tab", "label": "Products per tab", "min": 2, "max": 16, "step": 1, "default": 4 },
    {
      "type": "select",
      "id": "image_ratio",
      "label": "Image shape",
      "options": [
        { "value": "square", "label": "Square" },
        { "value": "portrait", "label": "Portrait" },
        { "value": "tall", "label": "Tall" },
        { "value": "landscape", "label": "Landscape" }
      ],
      "default": "portrait"
    },
    {
      "type": "select",
      "id": "card_style",
      "label": "Card style",
      "options": [
        { "value": "plain", "label": "Plain" },
        { "value": "card", "label": "Card" }
      ],
      "default": "plain"
    },
    { "type": "range", "id": "card_radius", "label": "Corner radius", "min": 0, "max": 24, "step": 2, "unit": "px", "default": 12 },
    { "type": "checkbox", "id": "show_vendor", "label": "Show vendor", "default": false },
    { "type": "checkbox", "id": "show_second_image", "label": "Show second image on hover", "default": true },
    { "type": "checkbox", "id": "show_quick_add", "label": "Show quick add", "info": "Products with options link to their page instead.", "default": false },
    { "type": "text", "id": "quick_add_label", "label": "Quick add button", "default": "Add to cart" },
    { "type": "text", "id": "choose_label", "label": "Link for products with options", "default": "Choose options" },
    { "type": "text", "id": "sale_label", "label": "Sale badge", "default": "Sale" },
    { "type": "text", "id": "sold_out_label", "label": "Sold out badge", "default": "Sold out" },
    { "type": "text", "id": "sale_price_label", "label": "Sale price label (screen readers)", "default": "Sale price" },
    { "type": "text", "id": "regular_price_label", "label": "Regular price label (screen readers)", "default": "Regular price" },

    { "type": "header", "content": "Layout" },
    { "type": "range", "id": "columns", "label": "Columns on desktop", "min": 2, "max": 6, "step": 1, "default": 4 },
    { "type": "range", "id": "columns_mobile", "label": "Columns on mobile", "min": 1, "max": 2, "step": 1, "default": 2 },
    {
      "type": "select",
      "id": "layout_mobile",
      "label": "Layout on mobile",
      "options": [
        { "value": "grid", "label": "Grid" },
        { "value": "row", "label": "Swipeable row" }
      ],
      "default": "grid"
    },
    {
      "type": "select",
      "id": "section_width",
      "label": "Width",
      "options": [
        { "value": "page", "label": "Page width" },
        { "value": "full", "label": "Full width" }
      ],
      "default": "page"
    },
    {
      "type": "select",
      "id": "text_alignment",
      "label": "Heading and tabs alignment on desktop",
      "options": [
        { "value": "left", "label": "Left" },
        { "value": "center", "label": "Center" }
      ],
      "default": "center"
    },
    {
      "type": "select",
      "id": "text_alignment_mobile",
      "label": "Heading and tabs alignment on mobile",
      "options": [
        { "value": "left", "label": "Left" },
        { "value": "center", "label": "Center" }
      ],
      "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": "#1F1B17" },
    { "type": "color", "id": "color_accent", "label": "Accent (buttons, badges)", "default": "#7A5A3A" },
    { "type": "color", "id": "color_accent_text", "label": "Text on accent", "default": "#FFFFFF" },
    { "type": "color", "id": "color_card", "label": "Card background", "default": "#FAF6F1" },

    { "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": 64 },
    { "type": "range", "id": "padding_bottom", "label": "Bottom padding on desktop", "min": 0, "max": 200, "step": 4, "unit": "px", "default": 64 },
    { "type": "range", "id": "padding_top_mobile", "label": "Top padding on mobile", "min": 0, "max": 160, "step": 4, "unit": "px", "default": 40 },
    { "type": "range", "id": "padding_bottom_mobile", "label": "Bottom padding on mobile", "min": 0, "max": 160, "step": 4, "unit": "px", "default": 40 },
    {
      "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": "Fade products in when switching tabs", "info": "Never when reduced motion is on.", "default": true }
  ],
  "blocks": [
    {
      "type": "tab",
      "name": "Tab",
      "settings": [
        { "type": "collection", "id": "collection", "label": "Collection" },
        { "type": "text", "id": "label", "label": "Tab label", "info": "Optional. Uses the collection's title when empty." }
      ]
    }
  ],
  "presets": [
    {
      "name": "Lib · Collection tabs",
      "blocks": [
        { "type": "tab", "settings": { "label": "New in" } },
        { "type": "tab", "settings": { "label": "Best sellers" } },
        { "type": "tab", "settings": { "label": "Sale" } }
      ]
    }
  ]
}
{% endschema %}
```

### snippets/lib-product-card.liquid

```liquid
{%- comment -%}
  MIT License · (c) 2026 Lintel · https://lintelagency.com/resources/shopify-sections
{%- endcomment -%}
{% doc %}
  Product card shared by the library's product sections: image (second image on hover), optional
  vendor, price with compare-at price, sale and sold-out badges, and an optional quick add. Quick add
  is a plain Shopify product form that posts the only variant to /cart/add; products with options
  link to the product page. Colors come from the parent section's --lib-* custom properties.
  With `template: true` it prints empty slots (data-slot) for a script to fill from product JSON,
  so cards built in the browser look exactly like cards built in Liquid.
  @param {product} [product] - The product; blank prints a placeholder card (theme editor only)
  @param {boolean} [template] - Print empty slots for a script instead of a product
  @param {string} [title_tag] - Tag for the product title (default 'h3')
  @param {string} [ratio] - Image shape: 'square' (default), 'portrait', 'tall' or 'landscape'
  @param {string} [sizes] - sizes attribute for the image
  @param {boolean} [eager] - Load the image immediately
  @param {boolean} [show_vendor] - Show the vendor above the title
  @param {boolean} [show_second_image] - Show the second image on hover
  @param {boolean} [show_quick_add] - Show the quick add button or the choose options link
  @param {string} [quick_add_label] - Quick add button text
  @param {string} [choose_label] - Link text for products with options
  @param {string} [sold_out_label] - Badge and button text for sold-out products
  @param {string} [sale_label] - Badge text for products on sale
  @param {string} [sale_price_label] - Screen-reader label before the sale price
  @param {string} [regular_price_label] - Screen-reader label before the compare-at price
  @param {string} [id_prefix] - Unique prefix for the title id (the quick add button points at it)
  @example
  {% render 'lib-product-card', product: product, ratio: 'portrait', show_quick_add: true, quick_add_label: 'Add to cart', id_prefix: section.id %}
{% enddoc %}
{%- liquid
  assign tag = title_tag | default: 'h3'
  assign shape = ratio | default: 'square'
  assign title_id = id_prefix | append: '-' | append: product.id
  assign on_sale = false
  if product.compare_at_price > product.price
    assign on_sale = true
  endif
  assign has_img2 = false
  if show_second_image and product.media.size > 1 and product.media[1].preview_image != blank
    assign has_img2 = true
  endif
-%}

{%- if template -%}
  <article class="lib-card">
    <div class="lib-card__media" data-ratio="{{ shape }}">
      <img class="lib-card__img" data-slot="image" alt="" loading="lazy" width="600" height="600" hidden>
      <span class="lib-card__badge lib-card__badge--sold-out" data-slot="sold-out" hidden>{{ sold_out_label | escape }}</span>
      <span class="lib-card__badge" data-slot="sale" hidden>{{ sale_label | escape }}</span>
    </div>
    <div class="lib-card__body">
      {%- if show_vendor -%}
        <p class="lib-card__vendor" data-slot="vendor"></p>
      {%- endif -%}
      <{{ tag }} class="lib-card__title"><a class="lib-card__link" data-slot="link" href=""></a></{{ tag }}>
      <p class="lib-card__price">
        <span class="lib-card__sr" data-slot="sale-label" hidden>{{ sale_price_label | escape }}</span>
        <span class="lib-card__price-now" data-slot="price"></span>
        <span class="lib-card__sr" data-slot="regular-label" hidden>{{ regular_price_label | escape }}</span>
        <s class="lib-card__price-was" data-slot="compare" hidden></s>
      </p>
    </div>
  </article>
{%- elsif product != blank -%}
  <article class="lib-card{% if product.available == false %} is-sold-out{% endif %}">
    <div class="lib-card__media" data-ratio="{{ shape }}">
      {%- render 'lib-image', image: product.featured_image, class: 'lib-card__img', sizes: sizes, eager: eager, placeholder: 'product-1' -%}
      {%- if has_img2 -%}
        <div class="lib-card__img2" aria-hidden="true">
          {%- render 'lib-image', image: product.media[1].preview_image, class: 'lib-card__img', sizes: sizes -%}
        </div>
      {%- endif -%}
      {%- if product.available == false and sold_out_label != blank -%}
        <span class="lib-card__badge lib-card__badge--sold-out">{{ sold_out_label | escape }}</span>
      {%- elsif on_sale and sale_label != blank -%}
        <span class="lib-card__badge">{{ sale_label | escape }}</span>
      {%- endif -%}
    </div>

    <div class="lib-card__body">
      {%- if show_vendor and product.vendor != blank -%}
        <p class="lib-card__vendor">{{ product.vendor | escape }}</p>
      {%- endif -%}
      <{{ tag }} class="lib-card__title" id="{{ title_id }}">
        <a class="lib-card__link" href="{{ product.url }}">{{ product.title | escape }}</a>
      </{{ tag }}>
      <p class="lib-card__price">
        {%- if on_sale -%}
          <span class="lib-card__sr">{{ sale_price_label | escape }}</span>
          <span class="lib-card__price-now lib-card__price-now--sale">{{ product.price | money }}</span>
          <span class="lib-card__sr">{{ regular_price_label | escape }}</span>
          <s class="lib-card__price-was">{{ product.compare_at_price | money }}</s>
        {%- else -%}
          <span class="lib-card__price-now">{{ product.price | money }}</span>
        {%- endif -%}
      </p>

      {%- if show_quick_add -%}
        <div class="lib-card__cta">
          {%- if product.has_only_default_variant -%}
            {%- form 'product', product, class: 'lib-card__form' -%}
              <input type="hidden" name="id" value="{{ product.selected_or_first_available_variant.id }}">
              <input type="hidden" name="quantity" value="1">
              <button
                type="submit"
                name="add"
                class="lib-card__add"
                aria-describedby="{{ title_id }}"
                {% if product.available == false %}
                  disabled
                {% endif %}
              >
                {%- if product.available -%}
                  {{- quick_add_label | escape -}}
                {%- else -%}
                  {{- sold_out_label | default: quick_add_label | escape -}}
                {%- endif -%}
              </button>
            {%- endform -%}
          {%- else -%}
            <a class="lib-card__add" href="{{ product.url }}" aria-describedby="{{ title_id }}">
              {{- choose_label | escape -}}
            </a>
          {%- endif -%}
        </div>
      {%- endif -%}
    </div>
  </article>
{%- else -%}
  <article class="lib-card">
    <div class="lib-card__media" data-ratio="{{ shape }}">
      {{ 'product-1' | placeholder_svg_tag: 'lib-card__img' }}
    </div>
    <div class="lib-card__body">
      <{{ tag }} class="lib-card__title">Example product title</{{ tag }}>
      <p class="lib-card__price"><span class="lib-card__price-now">{{ 4800 | money }}</span></p>
    </div>
  </article>
{%- endif -%}

{% stylesheet %}
  .lib-card {
    position: relative;
    display: flex;
    flex: 1;
    flex-direction: column;
    min-width: 0;
    border-radius: var(--lib-radius-card, 12px);
    color: var(--lib-text, #1f1b17);
  }
  [data-card='card'] .lib-card { background: var(--lib-card, #faf6f1); }
  .lib-card__media {
    position: relative;
    overflow: hidden;
    aspect-ratio: 1;
    border-radius: var(--lib-radius-card, 12px);
    background: color-mix(in srgb, var(--lib-text, #1f1b17) 6%, transparent);
  }
  .lib-card__media[data-ratio='portrait'] { aspect-ratio: 4 / 5; }
  .lib-card__media[data-ratio='tall'] { aspect-ratio: 2 / 3; }
  .lib-card__media[data-ratio='landscape'] { aspect-ratio: 4 / 3; }
  [data-card='card'] .lib-card__media { border-end-start-radius: 0; border-end-end-radius: 0; }
  .lib-card__img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform 0.6s cubic-bezier(0.2, 0.7, 0.2, 1);
  }
  svg.lib-card__img { fill: color-mix(in srgb, var(--lib-text, #1f1b17) 22%, transparent); }
  .lib-card__img2 { position: absolute; inset: 0; opacity: 0; transition: opacity 0.4s ease; }
  @media (hover: hover) {
    .lib-card:hover .lib-card__img2 { opacity: 1; }
    .lib-card:hover .lib-card__img { transform: scale(1.03); }
  }
  .lib-card:focus-within .lib-card__img2 { opacity: 1; }
  .lib-card.is-sold-out .lib-card__media img { opacity: 0.6; }

  .lib-card__badge {
    position: absolute;
    z-index: 2;
    top: 0.75rem;
    left: 0.75rem;
    padding: 0.3rem 0.7rem;
    border-radius: 999px;
    background: var(--lib-accent, #1f1b17);
    color: var(--lib-accent-text, #fff);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.03em;
    line-height: 1.2;
    pointer-events: none;
  }
  .lib-card__badge[hidden] { display: none; }
  .lib-card__badge--sold-out {
    background: var(--lib-bg, #fff);
    color: var(--lib-text, #1f1b17);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--lib-text, #1f1b17) 20%, transparent);
  }

  .lib-card__body { display: flex; flex: 1; flex-direction: column; gap: 0.3rem; padding-top: 0.9rem; }
  [data-card='card'] .lib-card__body { padding: 0.9rem 1rem 1rem; }
  .lib-card__vendor { margin: 0; font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; opacity: 0.7; }
  .lib-card__vendor:empty { display: none; }
  .lib-card__title { margin: 0; font-size: 1rem; font-weight: 600; line-height: 1.3; }
  .lib-card__link { color: inherit; text-decoration: none; }
  /* The title link covers the whole card; the quick add button sits above it. */
  .lib-card__link::after { content: ''; position: absolute; inset: 0; z-index: 1; border-radius: var(--lib-radius-card, 12px); }
  .lib-card__link:hover { text-decoration: underline; text-underline-offset: 0.2em; }
  .lib-card__link:focus-visible { outline: none; }
  .lib-card__link:focus-visible::after { outline: 2px solid var(--lib-accent, #1f1b17); outline-offset: 3px; }
  .lib-card__price { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; margin: 0; font-size: 0.9375rem; }
  .lib-card__price-now--sale { font-weight: 600; }
  .lib-card__price-was { opacity: 0.7; font-size: 0.875rem; }
  .lib-card__price-was[hidden] { display: none; }
  .lib-card__sr {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .lib-card__cta { position: relative; z-index: 2; margin-top: auto; padding-top: 0.75rem; }
  .lib-card__form { margin: 0; }
  .lib-card__add {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    min-height: 2.75rem;
    padding: 0.6rem 1rem;
    border: 1.5px solid var(--lib-text, #1f1b17);
    border-radius: min(var(--lib-radius-card, 12px), 999px);
    background: transparent;
    color: var(--lib-text, #1f1b17);
    font: inherit;
    font-size: 0.875rem;
    font-weight: 600;
    line-height: 1.2;
    text-align: center;
    text-decoration: none;
    cursor: pointer;
    transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
  }
  .lib-card__add:hover:not(:disabled) {
    background: var(--lib-accent, #1f1b17);
    border-color: var(--lib-accent, #1f1b17);
    color: var(--lib-accent-text, #fff);
  }
  .lib-card__add:focus-visible { outline: 2px solid var(--lib-accent, #1f1b17); outline-offset: 2px; }
  .lib-card__add:disabled { opacity: 0.45; cursor: default; }

  @media (prefers-reduced-motion: reduce) {
    .lib-card__img,
    .lib-card__img2,
    .lib-card__add { transition: none; }
    .lib-card:hover .lib-card__img { transform: none; }
  }
{% endstylesheet %}
```

### 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.
