# Newsletter banner: free Shopify section

> A compact email sign-up: a one-row strip or a footer-style block with optional image. Saves subscribers as Shopify customers; no app or JavaScript.

Newsletter banner is a free Shopify section for a compact email sign-up in two styles: a strip with the heading, a short line and the email field in one row, or a larger footer-style block with heading, text, form and small print beside an optional image. It uses Shopify's own customer form, so each subscriber is saved as a customer tagged "newsletter" plus an optional tag of your own, and it shows your success and error messages. It needs no JavaScript and works in Online Store 2.0 themes without an app.

- **Category:** Forms and newsletter
- **Use on:** Home, Footer area, Blog, Any page
- **Reads:** Shopify customer form
- **Size:** 20 KB in 2 files, no JavaScript
- **License:** MIT, © 2026 Lintel
- **Page:** https://lintelagency.com/resources/shopify-sections/newsletter-banner
- **Live demo:** https://lintelagency.com/resources/shopify-sections/demo/lib-newsletter-banner.html
- **Added:** 2026-11-02

## What it’s for

- A slim sign-up strip between sections on the home page or under blog articles
- A sign-up block placed right above the theme footer, with a photo beside it
- A waitlist sign-up on a coming-soon or launch page

## Editor settings

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

### Section settings

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `variant` | `select` | Style | strip | `strip`, `footer` |

### Content

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `heading` | `inline_richtext` | Heading | Get the studio letter |  |
| `heading_tag` | `select` | Heading tag | h2 | `h1`, `h2`, `h3`, `p` |
| `heading_size` | `select` | Heading size | medium | `small`, `medium`, `large` |
| `text` | `richtext` | Text | `<p>New pieces and kiln days, once a month.</p>` |  |

### Form

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `input_label` | `text` | Field label | Email address |  |
| `show_label` | `checkbox` | Show field label | `false` |  |
| `placeholder` | `text` | Field placeholder | you@example.com |  |
| `button_label` | `text` | Button label | Subscribe |  |
| `customer_tag` | `text` | Extra customer tag | – |  |
| `small_print` | `inline_richtext` | Small print | `Unsubscribe any time. Read our <a href="/policies/privacy-policy">privacy policy</a>.` |  |
| `success_message` | `text` | Success message | Thanks for subscribing. See you in your inbox. |  |
| `error_message` | `text` | Error message | We couldn't sign you up. Please check your email address. |  |

### Image (footer block)

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `image` | `image_picker` | Desktop image | – |  |
| `image_mobile` | `image_picker` | Mobile image | – |  |
| `image_position` | `select` | Image position on desktop | right | `left`, `right` |

### Layout

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `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 | #2A2420 |  |
| `color_bg_gradient` | `color_background` | Background gradient | – |  |
| `color_text` | `color` | Text | #F4EFE8 |  |
| `color_accent` | `color` | Accent (button) | #E8D9C4 |  |
| `color_accent_text` | `color` | Text on accent | #2A2420 |  |
| `color_field_bg` | `color` | Field background | #FFFDF9 |  |
| `color_field_text` | `color` | Field text | #2A2420 |  |

### Spacing and visibility

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `padding_top` | `range` | Top padding on desktop | `40` | 0–160 px, step 4 |
| `padding_bottom` | `range` | Bottom padding on desktop | `40` | 0–160 px, step 4 |
| `padding_top_mobile` | `range` | Top padding on mobile | `32` | 0–120 px, step 4 |
| `padding_bottom_mobile` | `range` | Bottom padding on mobile | `32` | 0–120 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` |  |

## 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-newsletter-banner.liquid` and paste the contents of `sections/lib-newsletter-banner.liquid` from the source below. Save.
4. In the `snippets` folder, add `lib-image.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 · Newsletter banner**. 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-newsletter-banner.liquid https://lintelagency.com/resources/shopify-sections/files/lib-newsletter-banner.liquid
curl -fsSL -o snippets/lib-image.liquid https://lintelagency.com/resources/shopify-sections/files/lib-image.liquid
shopify theme push --theme <unpublished-theme-id> --only sections/lib-newsletter-banner.liquid --only snippets/lib-image.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-newsletter-banner.liquid](https://lintelagency.com/resources/shopify-sections/files/lib-newsletter-banner.liquid): goes in `sections/`
- [snippets/lib-image.liquid](https://lintelagency.com/resources/shopify-sections/files/lib-image.liquid): goes in `snippets/`

## Questions

### How is it different from the Newsletter signup section?

Newsletter signup is a full section with centered, split-image and image-banner layouts. Newsletter banner is the compact version: a one-row strip that fits between other sections, or a footer-style block, with the same Shopify customer form.

### Where do the subscribers go?

Into your Shopify customers, tagged "newsletter" and, if you set one, your own tag (for example "footer"), so you can see where they signed up. Send to them with Shopify Email or an email app that syncs customers.

### What does a shopper see after signing up?

The page reloads at the section and the form is replaced by your success message. If the email is not accepted, your error message shows with Shopify's own explanation, and the field is marked invalid for screen readers.

### Can the strip sit inside the footer?

Place it as the last section of the template, or in the footer group in themes that let you add sections there; it takes the full width or the page width you choose.

## Source

### sections/lib-newsletter-banner.liquid

```liquid
{%- comment -%}
  MIT License · (c) 2026 Lintel · https://lintelagency.com/resources/shopify-sections
{%- endcomment -%}
{%- comment -%}
  Newsletter banner: a compact sign-up in two styles. "Strip" puts the heading, a short line and
  the email field with its button in one row on desktop (stacked on mobile), for between sections
  or above the footer. "Footer block" is a larger block with heading, text, form and small print,
  plus an optional image beside it. Both use Shopify's customer form: the email is added as a
  customer tagged "newsletter" (plus an optional tag of your own); success and error messages are
  settings. No JavaScript.
  Depends on: snippets/lib-image.liquid
{%- endcomment -%}

{%- liquid
  assign s = section.settings
  assign heading_tag = s.heading_tag | default: 'h2'
  assign form_id = 'lib-nl-banner-' | append: section.id
  assign input_id = 'LibNlBannerEmail-' | append: section.id
  assign error_id = 'LibNlBannerError-' | append: section.id
  assign note_id = 'LibNlBannerNote-' | append: section.id
  assign tags = 'newsletter'
  if s.customer_tag != blank
    assign tags = tags | append: ',' | append: s.customer_tag
  endif
  assign has_image = false
  if s.variant == 'footer' and s.image != blank or s.variant == 'footer' and s.image_mobile != blank
    assign has_image = true
  endif
-%}

<div
  class="lib-nl-banner lib-nl-banner--{{ s.variant }} lib-nl-banner--{{ s.section_width }}"
  {% if s.anchor_id != blank %}
    id="{{ s.anchor_id | handleize }}"
  {% endif %}
  data-show="{{ s.show_on }}"
  data-image-pos="{{ s.image_position }}"
  {% if has_image %}
    data-has-image
  {% 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-field-bg: {{ s.color_field_bg }};
    --lib-field-text: {{ s.color_field_text }};
    --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-nl-banner__inner">
    {%- if has_image -%}
      <div class="lib-nl-banner__media">
        {%- render 'lib-image',
          image: s.image,
          image_mobile: s.image_mobile,
          class: 'lib-nl-banner__img',
          sizes: '(min-width: 990px) 40vw, 100vw'
        -%}
      </div>
    {%- endif -%}

    <div class="lib-nl-banner__copy" data-align="{{ s.text_alignment }}" data-align-m="{{ s.text_alignment_mobile }}">
      {%- if s.heading != blank -%}
        <{{ heading_tag }} class="lib-nl-banner__heading lib-nl-banner__heading--{{ s.heading_size }}">
          {{- s.heading -}}
        </{{ heading_tag }}>
      {%- endif -%}
      {%- if s.text != blank -%}
        <div class="lib-nl-banner__text">{{ s.text }}</div>
      {%- endif -%}
    </div>

    {%- form 'customer', id: form_id, class: 'lib-nl-banner__form' -%}
      <input type="hidden" name="contact[tags]" value="{{ tags | escape }}">
      {%- if form.posted_successfully -%}
        <p class="lib-nl-banner__msg lib-nl-banner__msg--success" role="status">
          <svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><path d="m5 10.5 3.2 3L15 6.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
          <span>{{ s.success_message | escape }}</span>
        </p>
      {%- else -%}
        {%- if form.errors -%}
          <div class="lib-nl-banner__msg lib-nl-banner__msg--error" id="{{ error_id }}" role="alert">
            <p>{{ s.error_message | escape }}</p>
            {%- for field in form.errors -%}
              <p>
                {%- if field == 'form' -%}
                  {{ form.errors.messages[field] }}
                {%- else -%}
                  {{ form.errors.translated_fields[field] | capitalize }}
                  {{ form.errors.messages[field] }}
                {%- endif -%}
              </p>
            {%- endfor -%}
          </div>
        {%- endif -%}
        <label class="lib-nl-banner__label{% unless s.show_label %} lib-nl-banner__sr{% endunless %}" for="{{ input_id }}">
          {{- s.input_label | default: 'Email' | escape -}}
        </label>
        <div class="lib-nl-banner__row">
          <input
            class="lib-nl-banner__input"
            id="{{ input_id }}"
            type="email"
            name="contact[email]"
            required
            autocomplete="email"
            autocapitalize="off"
            spellcheck="false"
            {% if s.placeholder != blank %}
              placeholder="{{ s.placeholder | escape }}"
            {% endif %}
            {% if form.errors %}
              aria-invalid="true"
            {% endif %}
            {% if form.errors or s.small_print != blank %}
              aria-describedby="{% if form.errors %}{{ error_id }} {% endif %}{% if s.small_print != blank %}{{ note_id }}{% endif %}"
            {% endif %}
          >
          <button type="submit" class="lib-nl-banner__submit">{{ s.button_label | default: 'Subscribe' | escape }}</button>
        </div>
        {%- if s.small_print != blank -%}
          <p class="lib-nl-banner__note" id="{{ note_id }}">{{ s.small_print }}</p>
        {%- endif -%}
      {%- endif -%}
    {%- endform -%}
  </div>
</div>

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

  .lib-nl-banner__copy { display: flex; flex-direction: column; align-items: flex-start; text-align: left; }
  .lib-nl-banner__copy[data-align='center'] { align-items: center; text-align: center; }
  .lib-nl-banner__copy[data-align='right'] { align-items: flex-end; text-align: right; }
  @media (max-width: 749px) {
    .lib-nl-banner__copy[data-align-m='left'] { align-items: flex-start; text-align: left; }
    .lib-nl-banner__copy[data-align-m='center'] { align-items: center; text-align: center; }
    .lib-nl-banner__copy[data-align-m='right'] { align-items: flex-end; text-align: right; }
  }
  .lib-nl-banner__heading { margin: 0; font-size: clamp(1.375rem, 2.4vw, 1.875rem); line-height: 1.15; letter-spacing: -0.015em; text-wrap: balance; }
  .lib-nl-banner__heading--small { font-size: clamp(1.125rem, 1.8vw, 1.375rem); }
  .lib-nl-banner__heading--large { font-size: clamp(1.75rem, 3.4vw, 2.75rem); }
  .lib-nl-banner__text { margin-top: 0.5rem; font-size: 1rem; line-height: 1.55; opacity: 0.85; }
  .lib-nl-banner__text > :first-child { margin-top: 0; }
  .lib-nl-banner__text > :last-child { margin-bottom: 0; }

  .lib-nl-banner__form { width: 100%; }
  .lib-nl-banner__label { display: block; margin-bottom: 0.375rem; font-size: 0.875rem; font-weight: 600; }
  .lib-nl-banner__sr {
    position: absolute !important;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }
  .lib-nl-banner__row { display: flex; gap: 0.5rem; }
  .lib-nl-banner__input {
    flex: 1;
    min-width: 0;
    min-height: 3rem;
    padding: 0 1rem;
    border: 1.5px solid color-mix(in srgb, var(--lib-field-text) 30%, transparent);
    border-radius: 999px;
    background: var(--lib-field-bg);
    color: var(--lib-field-text);
    font: inherit;
  }
  .lib-nl-banner__input::placeholder { color: color-mix(in srgb, var(--lib-field-text) 60%, transparent); }
  .lib-nl-banner__input:focus-visible { outline: 2px solid var(--lib-accent); outline-offset: 2px; }
  .lib-nl-banner__submit {
    flex: none;
    min-height: 3rem;
    padding: 0 1.5rem;
    border: 0;
    border-radius: 999px;
    background: var(--lib-accent);
    color: var(--lib-accent-text);
    font: inherit;
    font-weight: 600;
    cursor: pointer;
  }
  .lib-nl-banner__submit:focus-visible { outline: 2px solid var(--lib-text); outline-offset: 2px; }
  @media (max-width: 479px) {
    .lib-nl-banner__row { flex-direction: column; }
  }
  .lib-nl-banner__note { margin: 0.625rem 0 0; font-size: 0.8125rem; line-height: 1.45; opacity: 0.8; }
  .lib-nl-banner__note a { color: inherit; text-underline-offset: 0.2em; }
  .lib-nl-banner__note a:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; border-radius: 2px; }
  .lib-nl-banner__msg { margin: 0 0 0.75rem; font-weight: 600; }
  .lib-nl-banner__msg p { margin: 0; }
  .lib-nl-banner__msg--success { display: flex; align-items: center; gap: 0.5rem; margin: 0; }
  .lib-nl-banner__msg--error { padding: 0.5rem 0.75rem; border-left: 3px solid currentColor; }

  /* Strip: copy and form side by side from tablet width up. */
  @media (min-width: 750px) {
    .lib-nl-banner--strip .lib-nl-banner__inner { grid-template-columns: 1fr minmax(20rem, 30rem); align-items: center; gap: 2.5rem; }
  }

  /* Footer block: copy over the form, optional image beside them. */
  .lib-nl-banner--footer .lib-nl-banner__inner { gap: 1.5rem; }
  .lib-nl-banner--footer .lib-nl-banner__copy,
  .lib-nl-banner--footer .lib-nl-banner__form { max-width: 34rem; }
  .lib-nl-banner--footer .lib-nl-banner__copy[data-align='center'] ~ .lib-nl-banner__form { margin-inline: auto; }
  .lib-nl-banner--footer .lib-nl-banner__heading { font-size: clamp(1.75rem, 3.4vw, 2.75rem); }
  .lib-nl-banner--footer .lib-nl-banner__heading--small { font-size: clamp(1.375rem, 2.2vw, 1.75rem); }
  .lib-nl-banner--footer .lib-nl-banner__text { margin-top: 0.875rem; font-size: 1.0625rem; }
  .lib-nl-banner__media {
    position: relative;
    overflow: hidden;
    border-radius: 14px;
    aspect-ratio: 4 / 3;
    background: color-mix(in srgb, var(--lib-text) 8%, transparent);
  }
  .lib-nl-banner__img { display: block; width: 100%; height: 100%; object-fit: cover; }
  .lib-nl-banner__img:is(svg) { fill: color-mix(in srgb, var(--lib-text) 25%, transparent); }
  @media (min-width: 990px) {
    .lib-nl-banner--footer[data-has-image] .lib-nl-banner__inner {
      grid-template-columns: 5fr 6fr;
      grid-template-rows: auto auto;
      column-gap: clamp(2.5rem, 5vw, 4.5rem);
      row-gap: 1.75rem;
      align-content: center;
    }
    .lib-nl-banner--footer[data-has-image] .lib-nl-banner__media { grid-row: 1 / 3; aspect-ratio: 1; }
    .lib-nl-banner--footer[data-has-image] .lib-nl-banner__copy { align-self: end; }
    .lib-nl-banner--footer[data-has-image] .lib-nl-banner__form { align-self: start; }
    .lib-nl-banner--footer[data-has-image][data-image-pos='right'] .lib-nl-banner__media { grid-column: 2; }
    .lib-nl-banner--footer[data-has-image][data-image-pos='right'] .lib-nl-banner__copy,
    .lib-nl-banner--footer[data-has-image][data-image-pos='right'] .lib-nl-banner__form { grid-column: 1; grid-row: auto; }
  }

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

{% schema %}
{
  "name": "Lib · Newsletter banner",
  "tag": "section",
  "class": "lib-section",
  "settings": [
    {
      "type": "select",
      "id": "variant",
      "label": "Style",
      "options": [
        { "value": "strip", "label": "Strip (one row from 750px)" },
        { "value": "footer", "label": "Footer block" }
      ],
      "default": "strip"
    },

    { "type": "header", "content": "Content" },
    { "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "Get the studio letter" },
    {
      "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": "Text", "default": "<p>New pieces and kiln days, once a month.</p>" },

    { "type": "header", "content": "Form" },
    { "type": "text", "id": "input_label", "label": "Field label", "default": "Email address" },
    { "type": "checkbox", "id": "show_label", "label": "Show field label", "info": "When off, the label is still read by screen readers.", "default": false },
    { "type": "text", "id": "placeholder", "label": "Field placeholder", "default": "you@example.com" },
    { "type": "text", "id": "button_label", "label": "Button label", "default": "Subscribe" },
    { "type": "text", "id": "customer_tag", "label": "Extra customer tag", "info": "Optional. Added next to \"newsletter\" on the customer." },
    { "type": "inline_richtext", "id": "small_print", "label": "Small print", "info": "Link your privacy policy here.", "default": "Unsubscribe any time. Read our <a href=\"/policies/privacy-policy\">privacy policy</a>." },
    { "type": "text", "id": "success_message", "label": "Success message", "default": "Thanks for subscribing. See you in your inbox." },
    { "type": "text", "id": "error_message", "label": "Error message", "info": "Shown above Shopify's own message when a sign-up fails.", "default": "We couldn't sign you up. Please check your email address." },

    { "type": "header", "content": "Image (footer block)" },
    { "type": "image_picker", "id": "image", "label": "Desktop image", "visible_if": "{{ section.settings.variant == 'footer' }}" },
    { "type": "image_picker", "id": "image_mobile", "label": "Mobile image", "info": "Optional. Shown below 750px.", "visible_if": "{{ section.settings.variant == 'footer' }}" },
    {
      "type": "select",
      "id": "image_position",
      "label": "Image position on desktop",
      "options": [
        { "value": "left", "label": "Left" },
        { "value": "right", "label": "Right" }
      ],
      "default": "right",
      "visible_if": "{{ section.settings.variant == 'footer' }}"
    },

    { "type": "header", "content": "Layout" },
    {
      "type": "select",
      "id": "section_width",
      "label": "Width",
      "options": [
        { "value": "page", "label": "Page width" },
        { "value": "full", "label": "Full width" }
      ],
      "default": "page"
    },
    { "type": "text_alignment", "id": "text_alignment", "label": "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", "default": "#2A2420" },
    { "type": "color_background", "id": "color_bg_gradient", "label": "Background gradient", "info": "Optional. Covers the background color." },
    { "type": "color", "id": "color_text", "label": "Text", "default": "#F4EFE8" },
    { "type": "color", "id": "color_accent", "label": "Accent (button)", "default": "#E8D9C4" },
    { "type": "color", "id": "color_accent_text", "label": "Text on accent", "default": "#2A2420" },
    { "type": "color", "id": "color_field_bg", "label": "Field background", "default": "#FFFDF9" },
    { "type": "color", "id": "color_field_text", "label": "Field text", "default": "#2A2420" },

    { "type": "header", "content": "Spacing and visibility" },
    { "type": "range", "id": "padding_top", "label": "Top padding on desktop", "min": 0, "max": 160, "step": 4, "unit": "px", "default": 40 },
    { "type": "range", "id": "padding_bottom", "label": "Bottom padding on desktop", "min": 0, "max": 160, "step": 4, "unit": "px", "default": 40 },
    { "type": "range", "id": "padding_top_mobile", "label": "Top padding on mobile", "min": 0, "max": 120, "step": 4, "unit": "px", "default": 32 },
    { "type": "range", "id": "padding_bottom_mobile", "label": "Bottom padding on mobile", "min": 0, "max": 120, "step": 4, "unit": "px", "default": 32 },
    {
      "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 }
  ],
  "presets": [
    { "name": "Lib · Newsletter banner" }
  ]
}
{% 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 %}
```

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