# Image collage: free Shopify section

> Two or three overlapping photos beside a text card with heading, text and a button, for About pages. A tidy grid on mobile. Liquid and CSS only.

Image collage is a free Shopify section that layers two or three photos into an overlapping collage beside a text card with an eyebrow, heading, text and a button. The images sit left or right, the card can overlap them on desktop, and on mobile the photos fall into a tidy grid (the first full width, the others side by side) above or below the card. Each image has its own mobile version and focal point. It is written in Liquid and CSS only, with no JavaScript, for Online Store 2.0 themes.

- **Category:** Content and storytelling
- **Use on:** About, Home, Landing pages
- **Reads:** Content only
- **Size:** 22 KB in 3 files, no JavaScript
- **License:** MIT, © 2026 Lintel
- **Page:** https://lintelagency.com/resources/shopify-sections/image-collage
- **Live demo:** https://lintelagency.com/resources/shopify-sections/demo/lib-image-collage.html
- **Added:** 2026-11-02

## What it’s for

- An About page block that shows the workshop, the materials and the packing table next to the brand's story
- A home page introduction to a new collection with a few lifestyle shots and a link to it
- A landing page section that tells how a product is made without a full gallery

## Editor settings

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

### Text card

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `eyebrow` | `text` | Eyebrow | Our story |  |
| `heading` | `inline_richtext` | Heading | `A small studio, <em>slowly</em> grown` |  |
| `heading_tag` | `select` | Heading tag | h2 | `h1`, `h2`, `h3`, `p` |
| `heading_size` | `select` | Heading size | large | `small`, `medium`, `large`, `xlarge` |
| `text` | `richtext` | Text | `<p>What started at a kitchen table is now a two-wheel studio. We still glaze in small batches, pack every order by hand and answer every email ourselves.</p>` |  |
| `button_label` | `text` | Button label | Read our story |  |
| `button_url` | `url` | Button link | – |  |
| `button_style` | `select` | Button style | primary | `primary`, `outline`, `text` |
| `button_new_tab` | `checkbox` | Open link in a new tab | `false` |  |

### Layout

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `images_position` | `select` | Images on desktop | left | `left`, `right` |
| `card_overlap` | `range` | Card overlaps images on desktop | `48` | 0–160 px, step 8 |
| `image_radius` | `range` | Image and card corner radius | `12` | 0–40 px, step 2 |
| `mobile_order` | `select` | Order on mobile | images_first | `images_first`, `text_first` |
| `section_width` | `select` | Width | page | `page`, `full` |
| `text_alignment` | `text_alignment` | Text alignment on desktop | left |  |
| `text_alignment_mobile` | `select` | Text alignment on mobile | left | `left`, `center`, `right` |

### Colors

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `color_bg` | `color` | Background | #F4EFE8 |  |
| `color_bg_gradient` | `color_background` | Background gradient | – |  |
| `color_text` | `color` | Text | #2A2420 |  |
| `color_accent` | `color` | Accent (solid button, eyebrow) | #7A5A3A |  |
| `color_accent_text` | `color` | Text on accent | #FFFFFF |  |
| `color_card` | `color` | Card background | #FFFDF9 |  |

### Spacing and visibility

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `padding_top` | `range` | Top padding on desktop | `88` | 0–200 px, step 4 |
| `padding_bottom` | `range` | Bottom padding on desktop | `88` | 0–200 px, step 4 |
| `padding_top_mobile` | `range` | Top padding on mobile | `48` | 0–160 px, step 4 |
| `padding_bottom_mobile` | `range` | Bottom padding on mobile | `56` | 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` | Rise in on scroll | `false` |  |

## Blocks

Up to 3 blocks in total.

### Image (`image`, up to 3)

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `image` | `image_picker` | Desktop image | – |  |
| `image_mobile` | `image_picker` | Mobile image | – |  |

## 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-image-collage.liquid` and paste the contents of `sections/lib-image-collage.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 · Image collage**. 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-image-collage.liquid https://lintelagency.com/resources/shopify-sections/files/lib-image-collage.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-image-collage.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-image-collage.liquid](https://lintelagency.com/resources/shopify-sections/files/lib-image-collage.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 many images does it take?

One to three; each is a block you can reorder in the theme editor. With three, the first is the largest and the other two overlap it; with two, they overlap diagonally; with one, it fills the image side.

### Does the collage still work on phones?

Yes, but without overlaps: below 750px the first image runs full width and the others sit side by side under it, so nothing covers anything on a small screen. You can put the text card before or after the images and give each image a separate mobile version.

### Where does the alt text come from?

From each image in Shopify's Files, where you set it once; screen readers read the images in block order, then the text card.

### Can the card overlap the images?

Yes. "Card overlaps images on desktop" moves the card over the collage by up to 160px; 0 keeps it beside the images. The card stays above the photos and keeps its own background color, so the text stays readable.

## Source

### sections/lib-image-collage.liquid

```liquid
{%- comment -%}
  MIT License · (c) 2026 Lintel · https://lintelagency.com/resources/shopify-sections
{%- endcomment -%}
{%- comment -%}
  Image collage: two or three overlapping images (one block each, with a separate mobile image and
  focal points) beside a text card with eyebrow, heading, text and a button. Images left or right;
  the card can overlap the collage on desktop. On mobile the images sit in a tidy grid (the first
  full width, the others side by side) above or below the card, without overlaps. No JavaScript.
  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 eager = false
  if section.index <= 2
    assign eager = true
  endif
  assign image_count = section.blocks.size
  if image_count > 3
    assign image_count = 3
  endif
-%}

<div
  class="lib-collage lib-collage--{{ s.section_width }}"
  {% if s.anchor_id != blank %}
    id="{{ s.anchor_id | handleize }}"
  {% endif %}
  data-show="{{ s.show_on }}"
  data-images="{{ s.images_position }}"
  data-count="{{ image_count }}"
  data-mobile-order="{{ s.mobile_order }}"
  {% if s.card_overlap > 0 %}
    data-overlap
  {% endif %}
  {% 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-radius-img: {{ s.image_radius }}px;
    --lib-overlap: {{ s.card_overlap }}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-collage__inner">
    <div class="lib-collage__images">
      {%- for block in section.blocks limit: 3 -%}
        {%- assign b = block.settings -%}
        <div class="lib-collage__frame lib-collage__frame--{{ forloop.index }}" {{ block.shopify_attributes }}>
          {%- liquid
            assign placeholder = 'lifestyle-' | append: forloop.index
            if forloop.index == 3
              assign placeholder = 'image'
            endif
            assign sizes = '(min-width: 990px) 30vw, 50vw'
            if forloop.first
              assign sizes = '(min-width: 990px) 35vw, 100vw'
            endif
            assign img_eager = false
            if eager and forloop.first
              assign img_eager = true
            endif
          -%}
          {%- render 'lib-image',
            image: b.image,
            image_mobile: b.image_mobile,
            class: 'lib-collage__img',
            sizes: sizes,
            eager: img_eager,
            placeholder: placeholder
          -%}
        </div>
      {%- endfor -%}
    </div>

    <div class="lib-collage__card" data-align="{{ s.text_alignment }}" data-align-m="{{ s.text_alignment_mobile }}">
      {%- if s.eyebrow != blank -%}
        <p class="lib-collage__eyebrow">{{ s.eyebrow | escape }}</p>
      {%- endif -%}
      {%- if s.heading != blank -%}
        <{{ heading_tag }} class="lib-collage__heading lib-collage__heading--{{ s.heading_size }}">
          {{- s.heading -}}
        </{{ heading_tag }}>
      {%- endif -%}
      {%- if s.text != blank -%}
        <div class="lib-collage__text">{{ s.text }}</div>
      {%- endif -%}
      {%- if s.button_label != blank -%}
        <div class="lib-collage__actions">
          {%- render 'lib-button',
            label: s.button_label,
            url: s.button_url,
            style: s.button_style,
            new_tab: s.button_new_tab
          -%}
        </div>
      {%- endif -%}
    </div>
  </div>
</div>

{% stylesheet %}
  .lib-collage {
    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-collage__inner {
    max-width: 1200px;
    margin: 0 auto;
    display: grid;
    gap: clamp(1.5rem, 4vw, 3rem);
  }
  .lib-collage--full .lib-collage__inner { max-width: none; }
  .lib-collage__inner > * { min-width: 0; }

  /* Mobile: a tidy grid, first image full width, the others side by side; no overlaps. */
  .lib-collage__images { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
  .lib-collage__frame {
    position: relative;
    overflow: hidden;
    border-radius: var(--lib-radius-img);
    background: color-mix(in srgb, var(--lib-text) 6%, transparent);
    aspect-ratio: 1;
  }
  .lib-collage__frame--1 { grid-column: 1 / -1; aspect-ratio: 4 / 3; }
  .lib-collage[data-count='2'] .lib-collage__frame--2 { grid-column: 1 / -1; aspect-ratio: 3 / 2; }
  .lib-collage__img { display: block; width: 100%; height: 100%; object-fit: cover; }
  .lib-collage__img:is(svg) { fill: color-mix(in srgb, var(--lib-text) 25%, transparent); }

  @media (max-width: 749px) {
    .lib-collage { padding-top: var(--lib-pt-m); padding-bottom: var(--lib-pb-m); }
    .lib-collage[data-show='desktop'] { display: none; }
    .lib-collage[data-mobile-order='text_first'] .lib-collage__card { order: -1; }
  }
  @media (min-width: 750px) {
    .lib-collage[data-show='mobile'] { display: none; }
  }

  /* Desktop: images overlap on a 12 × 12 grid; each later image sits on top with a ring in the
     background color, so the edges read clearly. */
  @media (min-width: 990px) {
    .lib-collage__inner { --lib-gap: clamp(2rem, 5vw, 4.5rem); grid-template-columns: 7fr 5fr; align-items: center; gap: var(--lib-gap); }
    .lib-collage[data-images='right'] .lib-collage__inner { grid-template-columns: 5fr 7fr; }
    .lib-collage[data-images='right'] .lib-collage__images { order: 2; }
    .lib-collage__images {
      grid-template-columns: repeat(12, 1fr);
      grid-template-rows: repeat(12, 1fr);
      gap: 0;
      aspect-ratio: 1 / 0.92;
    }
    .lib-collage__frame { aspect-ratio: auto; box-shadow: 0 0 0 8px var(--lib-bg), 0 24px 48px -28px rgb(0 0 0 / 0.35); }
    .lib-collage__frame--1 { grid-area: 1 / 1 / 13 / 13; aspect-ratio: auto; box-shadow: none; }
    .lib-collage[data-count='2'] .lib-collage__frame--1 { grid-area: 1 / 1 / 11 / 9; }
    .lib-collage[data-count='2'] .lib-collage__frame--2 { grid-area: 5 / 6 / 13 / 13; aspect-ratio: auto; z-index: 1; }
    .lib-collage[data-count='3'] .lib-collage__frame--1 { grid-area: 1 / 1 / 9 / 8; }
    .lib-collage[data-count='3'] .lib-collage__frame--2 { grid-area: 2 / 8 / 7 / 13; z-index: 1; }
    .lib-collage[data-count='3'] .lib-collage__frame--3 { grid-area: 7 / 4 / 13 / 11; z-index: 2; }
    .lib-collage[data-images='right'][data-count='3'] .lib-collage__frame--1 { grid-area: 1 / 6 / 9 / 13; }
    .lib-collage[data-images='right'][data-count='3'] .lib-collage__frame--2 { grid-area: 2 / 1 / 7 / 6; }
    .lib-collage[data-images='right'][data-count='3'] .lib-collage__frame--3 { grid-area: 7 / 3 / 13 / 10; }
    .lib-collage[data-images='right'][data-count='2'] .lib-collage__frame--1 { grid-area: 1 / 5 / 11 / 13; }
    .lib-collage[data-images='right'][data-count='2'] .lib-collage__frame--2 { grid-area: 5 / 1 / 13 / 8; }
    .lib-collage__card { position: relative; z-index: 3; }
    .lib-collage[data-overlap][data-images='left'] .lib-collage__card { margin-inline-start: calc(-1 * (var(--lib-overlap) + var(--lib-gap))); }
    .lib-collage[data-overlap][data-images='right'] .lib-collage__card { margin-inline-end: calc(-1 * (var(--lib-overlap) + var(--lib-gap))); }
  }

  .lib-collage__card {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    text-align: left;
    padding: clamp(1.75rem, 4vw, 3.25rem);
    border-radius: var(--lib-radius-img);
    background: var(--lib-card);
    box-shadow: 0 18px 40px -24px rgb(0 0 0 / 0.25);
  }
  .lib-collage__card[data-align='center'] { align-items: center; text-align: center; }
  .lib-collage__card[data-align='right'] { align-items: flex-end; text-align: right; }
  @media (max-width: 749px) {
    .lib-collage__card[data-align-m='left'] { align-items: flex-start; text-align: left; }
    .lib-collage__card[data-align-m='center'] { align-items: center; text-align: center; }
    .lib-collage__card[data-align-m='right'] { align-items: flex-end; text-align: right; }
  }
  .lib-collage__eyebrow {
    margin: 0 0 1rem;
    font-size: 0.8rem;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--lib-accent);
  }
  .lib-collage__heading {
    margin: 0;
    font-size: clamp(1.75rem, 3.4vw, 2.75rem);
    line-height: 1.1;
    letter-spacing: -0.02em;
    text-wrap: balance;
  }
  .lib-collage__heading--small { font-size: clamp(1.375rem, 2.2vw, 1.75rem); }
  .lib-collage__heading--medium { font-size: clamp(1.5rem, 2.8vw, 2.25rem); }
  .lib-collage__heading--xlarge { font-size: clamp(2.25rem, 5vw, 3.75rem); }
  .lib-collage__text { margin-top: 1rem; max-width: 34rem; font-size: 1.0625rem; line-height: 1.65; opacity: 0.82; }
  .lib-collage__text > :first-child { margin-top: 0; }
  .lib-collage__text > :last-child { margin-bottom: 0; }
  .lib-collage__text a { color: inherit; text-underline-offset: 0.2em; }
  .lib-collage__actions { margin-top: 1.75rem; }

  /* Motion: images and card rise in as they scroll into view, the later images a little later. */
  @supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
      .lib-collage[data-animate] .lib-collage__frame,
      .lib-collage[data-animate] .lib-collage__card {
        animation: lib-collage-rise linear both;
        animation-timeline: view();
        animation-range: entry 0% entry 55%;
      }
      .lib-collage[data-animate] .lib-collage__frame--2 { animation-range: entry 10% entry 70%; }
      .lib-collage[data-animate] .lib-collage__frame--3 { animation-range: entry 20% entry 85%; }
    }
  }
  @keyframes lib-collage-rise {
    from { opacity: 0; transform: translateY(28px); }
  }
{% endstylesheet %}

{% schema %}
{
  "name": "Lib · Image collage",
  "tag": "section",
  "class": "lib-section",
  "max_blocks": 3,
  "settings": [
    { "type": "header", "content": "Text card" },
    { "type": "text", "id": "eyebrow", "label": "Eyebrow", "default": "Our story" },
    { "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "A small studio, <em>slowly</em> grown" },
    {
      "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": "large"
    },
    { "type": "richtext", "id": "text", "label": "Text", "default": "<p>What started at a kitchen table is now a two-wheel studio. We still glaze in small batches, pack every order by hand and answer every email ourselves.</p>" },
    { "type": "text", "id": "button_label", "label": "Button label", "default": "Read our story" },
    { "type": "url", "id": "button_url", "label": "Button link" },
    {
      "type": "select",
      "id": "button_style",
      "label": "Button style",
      "options": [
        { "value": "primary", "label": "Solid" },
        { "value": "outline", "label": "Outline" },
        { "value": "text", "label": "Text link" }
      ],
      "default": "primary"
    },
    { "type": "checkbox", "id": "button_new_tab", "label": "Open link in a new tab", "default": false },

    { "type": "header", "content": "Layout" },
    {
      "type": "select",
      "id": "images_position",
      "label": "Images on desktop",
      "options": [
        { "value": "left", "label": "Left" },
        { "value": "right", "label": "Right" }
      ],
      "default": "left"
    },
    { "type": "range", "id": "card_overlap", "label": "Card overlaps images on desktop", "info": "0 keeps the card beside the images.", "min": 0, "max": 160, "step": 8, "unit": "px", "default": 48 },
    { "type": "range", "id": "image_radius", "label": "Image and card corner radius", "min": 0, "max": 40, "step": 2, "unit": "px", "default": 12 },
    {
      "type": "select",
      "id": "mobile_order",
      "label": "Order on mobile",
      "options": [
        { "value": "images_first", "label": "Images first" },
        { "value": "text_first", "label": "Text first" }
      ],
      "default": "images_first"
    },
    {
      "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": "Text alignment on desktop", "default": "left" },
    {
      "type": "select",
      "id": "text_alignment_mobile",
      "label": "Text 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", "info": "Also the thin ring between overlapping images.", "default": "#F4EFE8" },
    { "type": "color_background", "id": "color_bg_gradient", "label": "Background gradient", "info": "Optional. Covers the background color." },
    { "type": "color", "id": "color_text", "label": "Text", "default": "#2A2420" },
    { "type": "color", "id": "color_accent", "label": "Accent (solid button, eyebrow)", "default": "#7A5A3A" },
    { "type": "color", "id": "color_accent_text", "label": "Text on accent", "default": "#FFFFFF" },
    { "type": "color", "id": "color_card", "label": "Card background", "default": "#FFFDF9" },

    { "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": 88 },
    { "type": "range", "id": "padding_bottom", "label": "Bottom padding on desktop", "min": 0, "max": 200, "step": 4, "unit": "px", "default": 88 },
    { "type": "range", "id": "padding_top_mobile", "label": "Top padding on mobile", "min": 0, "max": 160, "step": 4, "unit": "px", "default": 48 },
    { "type": "range", "id": "padding_bottom_mobile", "label": "Bottom padding on mobile", "min": 0, "max": 160, "step": 4, "unit": "px", "default": 56 },
    {
      "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": "Rise in on scroll", "info": "Only in browsers that support scroll-driven animations; never when reduced motion is on.", "default": false }
  ],
  "blocks": [
    {
      "type": "image",
      "name": "Image",
      "limit": 3,
      "settings": [
        { "type": "image_picker", "id": "image", "label": "Desktop image", "info": "The first image is the largest. Alt text comes from the image in Files." },
        { "type": "image_picker", "id": "image_mobile", "label": "Mobile image", "info": "Optional. Shown below 750px." }
      ]
    }
  ],
  "presets": [
    {
      "name": "Lib · Image collage",
      "blocks": [
        { "type": "image" },
        { "type": "image" },
        { "type": "image" }
      ]
    }
  ]
}
{% 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.
