# Timeline: free Shopify section

> Brand history or a process as dated milestones on a line, alternating sides on desktop and stacked on mobile, with images and links. Liquid and CSS only.

Timeline is a free Shopify section that tells a brand's history, or the stages of a process, as dated milestones on a line. On desktop the milestones alternate either side of a central line or sit on one side; on mobile they stack on one side. Each milestone has a date label (any text, such as 2019 or Spring 2021), a title, text, an optional image with a separate mobile image and focal points, and an optional link. It is an ordered list in plain Liquid and CSS, with no JavaScript, for Online Store 2.0 themes.

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

## What it’s for

- The story of the brand on an About page, from the first product to today
- How a made-to-order product is produced, from order to delivery
- A collaboration, restoration or renovation documented step by step

## Editor settings

In the theme editor the section is called “Lib · Timeline”. 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 | Our story so far |  |
| `heading_tag` | `select` | Heading tag | h2 | `h1`, `h2`, `h3`, `p` |
| `heading_size` | `select` | Heading size | medium | `small`, `medium`, `large` |
| `text` | `richtext` | Intro text | – |  |

### Layout

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `layout` | `select` | Layout on desktop | alternate | `alternate`, `single` |
| `item_style` | `select` | Milestone style | card | `plain`, `card` |
| `image_ratio` | `select` | Image shape | standard | `standard`, `landscape`, `square` |
| `section_width` | `select` | Width | page | `page`, `full` |
| `text_alignment` | `select` | Heading alignment on desktop | center | `left`, `center` |
| `text_alignment_mobile` | `select` | Heading alignment on mobile | left | `left`, `center` |

### Colors

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

### Spacing and visibility

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `padding_top` | `range` | Top padding on desktop | `80` | 0–200 px, step 4 |
| `padding_bottom` | `range` | Bottom padding on desktop | `80` | 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 | `48` | 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 milestones in on scroll | `true` |  |

## Blocks

Up to 16 blocks in total.

### Milestone (`milestone`)

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `date` | `text` | Date | 2019 |  |
| `title` | `text` | Title | Milestone title |  |
| `text` | `richtext` | Text | `<p>A sentence or two about what happened and why it mattered.</p>` |  |
| `image` | `image_picker` | Image | – |  |
| `image_mobile` | `image_picker` | Mobile image | – |  |
| `link_label` | `text` | Link text | – |  |
| `link_url` | `url` | Link | – |  |
| `link_new_tab` | `checkbox` | Open link in a new tab | `false` |  |

## 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-timeline.liquid` and paste the contents of `sections/lib-timeline.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 · Timeline**. 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-timeline.liquid https://lintelagency.com/resources/shopify-sections/files/lib-timeline.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-timeline.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-timeline.liquid](https://lintelagency.com/resources/shopify-sections/files/lib-timeline.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 milestones can I add?

Up to 16, each a block you can reorder in the theme editor. The date label is free text, so it can be a year, a season or "Step 1".

### How does it look on mobile?

The line moves to the left and every milestone stacks to its right, whatever the desktop layout. You can also give each milestone a separate mobile image.

### Is it readable by screen readers and search engines?

Yes. The milestones are an ordered list in the HTML, in your order, with real headings; the line and markers are decoration only. There is no JavaScript.

### Can milestones fade in as you scroll?

Yes, with the motion setting, in browsers that support scroll-driven CSS animations. It's never on when a visitor prefers reduced motion, and the content is visible without it.

## Source

### sections/lib-timeline.liquid

```liquid
{%- comment -%}
  MIT License · (c) 2026 Lintel · https://lintelagency.com/resources/shopify-sections
{%- endcomment -%}
{%- comment -%}
  Timeline: dated milestones for a brand's story or a process, as an ordered list on a line.
  On desktop the milestones alternate either side of the line or sit on one side; on mobile they
  always stack on one side. Each milestone has a date label, title, text, optional image (separate
  mobile image, focal points) and an optional link. Pure Liquid and CSS; 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 item_tag = 'h3'
  if heading_tag == 'h3' or heading_tag == 'p'
    assign item_tag = 'h4'
  endif
-%}

<div
  class="lib-timeline lib-timeline--{{ s.section_width }}"
  {% if s.anchor_id != blank %}
    id="{{ s.anchor_id | handleize }}"
  {% endif %}
  data-show="{{ s.show_on }}"
  data-layout="{{ s.layout }}"
  data-style="{{ s.item_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-line: {{ s.color_line }};
    --lib-card: {{ s.color_card }};
    --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-timeline__inner">
    {%- if s.heading != blank or s.text != blank -%}
      <header class="lib-timeline__header" data-align="{{ s.text_alignment }}" data-align-m="{{ s.text_alignment_mobile }}">
        {%- if s.heading != blank -%}
          <{{ heading_tag }} class="lib-timeline__heading lib-timeline__heading--{{ s.heading_size }}">
            {{- s.heading -}}
          </{{ heading_tag }}>
        {%- endif -%}
        {%- if s.text != blank -%}
          <div class="lib-timeline__text">{{ s.text }}</div>
        {%- endif -%}
      </header>
    {%- endif -%}

    {%- if section.blocks.size > 0 -%}
      <ol class="lib-timeline__list" role="list">
        {%- for block in section.blocks -%}
          {%- assign b = block.settings -%}
          <li class="lib-timeline__item{% if b.image != blank or b.image_mobile != blank %} has-image{% endif %}" {{ block.shopify_attributes }}>
            <span class="lib-timeline__marker" aria-hidden="true"></span>
            <div class="lib-timeline__card">
              {%- if b.date != blank -%}
                <p class="lib-timeline__date">{{ b.date | escape }}</p>
              {%- endif -%}
              {%- if b.title != blank -%}
                <{{ item_tag }} class="lib-timeline__title">{{ b.title | escape }}</{{ item_tag }}>
              {%- endif -%}
              {%- if b.image != blank or b.image_mobile != blank -%}
                <div class="lib-timeline__media" data-ratio="{{ s.image_ratio }}">
                  {%- render 'lib-image', image: b.image, image_mobile: b.image_mobile, class: 'lib-timeline__img', sizes: '(min-width: 990px) 480px, 90vw' -%}
                </div>
              {%- endif -%}
              {%- if b.text != blank -%}
                <div class="lib-timeline__body">{{ b.text }}</div>
              {%- endif -%}
              {%- if b.link_label != blank -%}
                <div class="lib-timeline__link">
                  {%- render 'lib-button', label: b.link_label, url: b.link_url, style: 'text', new_tab: b.link_new_tab, arrow: true -%}
                </div>
              {%- endif -%}
            </div>
          </li>
        {%- endfor -%}
      </ol>
    {%- endif -%}
  </div>
</div>

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

  .lib-timeline__header { max-width: 42rem; margin-bottom: clamp(2rem, 5vw, 3.5rem); }
  .lib-timeline__heading {
    margin: 0;
    font-size: clamp(1.75rem, 3vw, 2.5rem);
    line-height: 1.15;
    letter-spacing: -0.02em;
    text-wrap: balance;
  }
  .lib-timeline__heading--small { font-size: clamp(1.25rem, 1.8vw, 1.5rem); }
  .lib-timeline__heading--large { font-size: clamp(2rem, 4vw, 3.25rem); }
  .lib-timeline__text { margin-top: 0.75rem; font-size: 1rem; line-height: 1.6; opacity: 0.85; }
  .lib-timeline__text > :first-child { margin-top: 0; }
  .lib-timeline__text > :last-child { margin-bottom: 0; }
  .lib-timeline__header[data-align='center'] { margin-inline: auto; text-align: center; }
  @media (max-width: 749px) {
    .lib-timeline__header[data-align-m='left'] { margin-inline: 0; text-align: left; }
    .lib-timeline__header[data-align-m='center'] { margin-inline: auto; text-align: center; }
  }

  /* One side: line on the left. Mobile always uses this. */
  .lib-timeline__list {
    --lib-gutter: 2.25rem;
    position: relative;
    display: grid;
    gap: clamp(1.75rem, 4vw, 2.75rem);
    margin: 0;
    padding: 0 0 0 var(--lib-gutter);
    list-style: none;
  }
  .lib-timeline__list::before {
    content: '';
    position: absolute;
    top: 0.5rem;
    bottom: 0.5rem;
    left: calc(0.5rem - 1px);
    width: 2px;
    background: var(--lib-line);
  }
  .lib-timeline__item { position: relative; min-width: 0; }
  .lib-timeline__marker {
    position: absolute;
    top: 0.3rem;
    left: calc(var(--lib-gutter) * -1);
    width: 1rem;
    height: 1rem;
    border: 3px solid var(--lib-bg);
    border-radius: 50%;
    background: var(--lib-accent);
    box-shadow: 0 0 0 2px var(--lib-accent);
  }
  .lib-timeline__card { max-width: 34rem; }
  .lib-timeline[data-style='card'] .lib-timeline__card {
    padding: clamp(1.125rem, 2.5vw, 1.75rem);
    border-radius: 14px;
    background: var(--lib-card);
  }
  .lib-timeline__date {
    display: inline-block;
    margin: 0 0 0.5rem;
    padding: 0.2rem 0.65rem;
    border-radius: 999px;
    background: var(--lib-accent);
    color: var(--lib-accent-text);
    font-size: 0.8125rem;
    font-weight: 700;
    letter-spacing: 0.02em;
  }
  .lib-timeline__title { margin: 0; font-size: clamp(1.125rem, 1.6vw, 1.3125rem); line-height: 1.3; }
  .lib-timeline__media {
    overflow: hidden;
    margin-top: 0.875rem;
    border-radius: 10px;
    aspect-ratio: 4 / 3;
    background: color-mix(in srgb, var(--lib-text) 6%, transparent);
  }
  .lib-timeline__media[data-ratio='landscape'] { aspect-ratio: 16 / 9; }
  .lib-timeline__media[data-ratio='square'] { aspect-ratio: 1; }
  .lib-timeline__img { display: block; width: 100%; height: 100%; object-fit: cover; }
  .lib-timeline__body { margin-top: 0.625rem; font-size: 0.9375rem; line-height: 1.6; opacity: 0.85; }
  .lib-timeline__body > :first-child { margin-top: 0; }
  .lib-timeline__body > :last-child { margin-bottom: 0; }
  .lib-timeline__link { margin-top: 0.625rem; font-size: 0.9375rem; }

  /* Alternating: the line runs down the middle; milestones take turns on either side, the right
     column a little lower, so each pair overlaps instead of leaving a gap the height of a card. */
  @media (min-width: 750px) {
    .lib-timeline[data-layout='alternate'] .lib-timeline__list {
      grid-template-columns: repeat(2, minmax(0, 1fr));
      column-gap: calc(var(--lib-gutter) * 2);
      padding-left: 0;
    }
    .lib-timeline[data-layout='alternate'] .lib-timeline__list::before { left: calc(50% - 1px); }
    .lib-timeline[data-layout='alternate'] .lib-timeline__item { grid-column: 1; }
    .lib-timeline[data-layout='alternate'] .lib-timeline__card { margin-left: auto; text-align: right; }
    .lib-timeline[data-layout='alternate'] .lib-timeline__marker { left: auto; right: calc(var(--lib-gutter) * -1 - 0.5rem); }
    .lib-timeline[data-layout='alternate'] .lib-timeline__item:nth-child(even) {
      grid-column: 2;
      margin-top: clamp(3rem, 9vw, 8rem);
    }
    .lib-timeline[data-layout='alternate'] .lib-timeline__item:nth-child(even) .lib-timeline__card { margin-left: 0; text-align: left; }
    .lib-timeline[data-layout='alternate'] .lib-timeline__item:nth-child(even) .lib-timeline__marker { right: auto; left: calc(var(--lib-gutter) * -1 - 0.5rem); }
  }

  @supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
      .lib-timeline[data-animate] .lib-timeline__card {
        animation: lib-timeline-rise linear both;
        animation-timeline: view();
        animation-range: entry 0% entry 55%;
      }
    }
  }
  @keyframes lib-timeline-rise {
    from { opacity: 0; transform: translateY(20px); }
  }
{% endstylesheet %}

{% schema %}
{
  "name": "Lib · Timeline",
  "tag": "section",
  "class": "lib-section",
  "max_blocks": 16,
  "settings": [
    { "type": "header", "content": "Content" },
    { "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "Our story so far" },
    {
      "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": "header", "content": "Layout" },
    {
      "type": "select",
      "id": "layout",
      "label": "Layout on desktop",
      "info": "Mobile always shows one side.",
      "options": [
        { "value": "alternate", "label": "Alternating sides" },
        { "value": "single", "label": "One side" }
      ],
      "default": "alternate"
    },
    {
      "type": "select",
      "id": "item_style",
      "label": "Milestone style",
      "options": [
        { "value": "plain", "label": "Plain" },
        { "value": "card", "label": "Card" }
      ],
      "default": "card"
    },
    {
      "type": "select",
      "id": "image_ratio",
      "label": "Image shape",
      "options": [
        { "value": "standard", "label": "4:3" },
        { "value": "landscape", "label": "16:9" },
        { "value": "square", "label": "Square" }
      ],
      "default": "standard"
    },
    {
      "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 alignment on desktop",
      "options": [
        { "value": "left", "label": "Left" },
        { "value": "center", "label": "Center" }
      ],
      "default": "center"
    },
    {
      "type": "select",
      "id": "text_alignment_mobile",
      "label": "Heading 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": "#FAF6F1" },
    { "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 (markers, dates)", "default": "#7A5A3A" },
    { "type": "color", "id": "color_accent_text", "label": "Text on accent", "default": "#FFFFFF" },
    { "type": "color", "id": "color_line", "label": "Line", "default": "#DCCFBF" },
    { "type": "color", "id": "color_card", "label": "Card background", "default": "#FFFFFF" },

    { "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": 80 },
    { "type": "range", "id": "padding_bottom", "label": "Bottom padding on desktop", "min": 0, "max": 200, "step": 4, "unit": "px", "default": 80 },
    { "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": 48 },
    {
      "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 milestones in on scroll", "info": "Only in browsers that support scroll-driven animations; never when reduced motion is on.", "default": true }
  ],
  "blocks": [
    {
      "type": "milestone",
      "name": "Milestone",
      "settings": [
        { "type": "text", "id": "date", "label": "Date", "info": "Any label: 2019, Spring 2021, Step 1…", "default": "2019" },
        { "type": "text", "id": "title", "label": "Title", "default": "Milestone title" },
        { "type": "richtext", "id": "text", "label": "Text", "default": "<p>A sentence or two about what happened and why it mattered.</p>" },
        { "type": "image_picker", "id": "image", "label": "Image" },
        { "type": "image_picker", "id": "image_mobile", "label": "Mobile image", "info": "Optional. Below 750px." },
        { "type": "text", "id": "link_label", "label": "Link text" },
        { "type": "url", "id": "link_url", "label": "Link" },
        { "type": "checkbox", "id": "link_new_tab", "label": "Open link in a new tab", "default": false }
      ]
    }
  ],
  "presets": [
    {
      "name": "Lib · Timeline",
      "blocks": [
        { "type": "milestone", "settings": { "date": "2019", "title": "The first wheel" } },
        { "type": "milestone", "settings": { "date": "2021", "title": "Our own kiln" } },
        { "type": "milestone", "settings": { "date": "2024", "title": "The studio shop opens" } }
      ]
    }
  ]
}
{% 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.
