# Newsletter popup: free Shopify section

> Email sign-up popup with an optional discount code that opens after a delay, on exit intent or on scroll, never on load. A small card on mobile; no app.

Newsletter popup is a free Shopify section that offers a newsletter sign-up, with an optional image and discount code, in a popup that opens after a delay, when the pointer leaves the window on desktop, or after the shopper scrolls part of the page, and never as the page loads. It uses Shopify's own customer form, so each email is saved as a customer tagged "newsletter" plus a tag you choose. On phones it is a small card at the bottom of the screen rather than a full-screen overlay; on desktop it is a centered dialog or a corner card. It closes with a button, Esc or a click outside, stays away for the number of days you set and for good after a sign-up, and shows at most once per visit. The discount itself is created in Shopify; the section only shows the code.

- **Category:** Conversion and offers
- **Use on:** Home, Collection, Product, Any page
- **Reads:** Shopify customer form
- **Size:** 25 KB in 1 file, with a small script
- **License:** MIT, © 2026 Lintel
- **Page:** https://lintelagency.com/resources/shopify-sections/newsletter-popup
- **Live demo:** https://lintelagency.com/resources/shopify-sections/demo/lib-newsletter-popup.html
- **Added:** 2026-11-02

## What it’s for

- A welcome offer, such as a first-order code, for new visitors
- Collecting emails before a launch or a seasonal sale
- An exit-intent reminder on collection and product pages

## Editor settings

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

### Offer

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `heading` | `inline_richtext` | Heading | 10% off your first order |  |
| `heading_tag` | `select` | Heading tag | h2 | `h2`, `h3`, `p` |
| `heading_size` | `select` | Heading size | medium | `small`, `medium`, `large` |
| `text` | `richtext` | Text | `<p>Join the studio letter for new pieces and kiln days. One email a month.</p>` |  |
| `image` | `image_picker` | Image | – |  |
| `discount_code` | `text` | Discount code | – |  |
| `code_label` | `text` | Code label | Use code |  |
| `copy_label` | `text` | Copy button | Copy |  |
| `copied_label` | `text` | Copied message | Code copied |  |

### Form

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `input_label` | `text` | Field label | Email address |  |
| `placeholder` | `text` | Field placeholder | you@example.com |  |
| `button_label` | `text` | Button label | Subscribe |  |
| `customer_tag` | `text` | Extra customer tag | popup |  |
| `consent` | `inline_richtext` | Consent text | `Unsubscribe any time. See our <a href="/policies/privacy-policy">privacy policy</a>.` |  |
| `success_message` | `text` | Success message | Thanks for joining. Check your inbox for the code. |  |
| `close_label` | `text` | Close button label | Close |  |

### When it opens

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `trigger_delay` | `checkbox` | After a delay | `true` |  |
| `delay_seconds` | `range` | Delay | `20` | 10–120 s, step 5 |
| `trigger_exit` | `checkbox` | When the pointer leaves the window | `true` |  |
| `trigger_scroll` | `checkbox` | After scrolling | `false` |  |
| `scroll_percent` | `range` | Scrolled share of the page | `50` | 20–90 %, step 5 |
| `remember_days` | `range` | Stay closed for | `14` | 1–90 d, step 1 |

### Layout

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `desktop_style` | `select` | On desktop | modal | `modal`, `corner` |
| `text_alignment` | `text_alignment` | Text alignment on desktop | left |  |
| `text_alignment_mobile` | `select` | Text alignment on mobile | left | `left`, `center`, `right` |
| `show_on` | `select` | Show on | all | `all`, `desktop`, `mobile` |

### Colors

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `color_bg` | `color` | Background | #FFFDF9 |  |
| `color_bg_gradient` | `color_background` | Background gradient | – |  |
| `color_text` | `color` | Text | #2A2420 |  |
| `color_accent` | `color` | Accent (button, focus ring) | #2A2420 |  |
| `color_accent_text` | `color` | Text on accent | #FFFDF9 |  |
| `backdrop_opacity` | `range` | Page dimming behind the dialog | `45` | 0–80 %, step 5 |

### Motion

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `animate` | `checkbox` | Fade in | `true` |  |

## Install

### In Shopify admin

1. Work on a copy, not your live theme: in Shopify admin go to **Online Store > Themes**, open the **…** menu on your current theme and choose **Duplicate**.
2. On the copy, open the **…** menu and choose **Edit code**.
3. In the `sections` folder, add a new file named `lib-newsletter-popup.liquid` and paste the contents of `sections/lib-newsletter-popup.liquid` from the source below. Save.
4. Open **Customize** on the copy, choose **Add section** and pick **Lib · Newsletter popup**. Replace the sample content with your own and set the colors.
5. 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-popup.liquid https://lintelagency.com/resources/shopify-sections/files/lib-newsletter-popup.liquid
shopify theme push --theme <unpublished-theme-id> --only sections/lib-newsletter-popup.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-popup.liquid](https://lintelagency.com/resources/shopify-sections/files/lib-newsletter-popup.liquid): goes in `sections/`

## Questions

### Will it hurt SEO or annoy mobile shoppers?

It never opens as the page loads, so the content a visitor came for shows first. Google warns against popups that cover the main content right after a visitor arrives from search, mostly on mobile; below 750px this section is a small card at the bottom of the screen that leaves the page readable and usable, not a full-screen interstitial.

### Where do the emails go?

To your Shopify customers, through Shopify's customer form, tagged "newsletter" plus the extra tag you set ("popup" by default) so you can tell these sign-ups apart. Shopify Email, or an email app that syncs customers, can then send to them.

### Does the discount code work by itself?

No. Create the discount in Shopify under Discounts; the section only shows the code you type, with a copy button.

### How often does it show?

At most once per visit. After a shopper closes it, it stays away for the number of days you set (1 to 90); after a sign-up it stays away for a year. If you change the heading or the code, it counts as a new offer and shows again to people who closed the old one. Only the choice is stored, in the shopper's browser; no personal data.

### Is it accessible?

On desktop it is a native modal dialog labelled by its heading: focus moves into it, Esc or the close button closes it, and focus goes back to where it was. The mobile and corner cards close with Esc too. The field has a real label, every word is a setting, and the fade-in is off when the shopper prefers reduced motion. Without JavaScript the popup doesn't appear at all, so it never blocks the page.

### How do I edit it in the theme editor?

It opens while the section is selected in the editor and closes when you select something else; the timers don't run in the editor.

## Source

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

```liquid
{%- comment -%}
  MIT License · (c) 2026 Lintel · https://lintelagency.com/resources/shopify-sections
{%- endcomment -%}
{%- comment -%}
  Newsletter popup: an offer with Shopify's customer form (the email is added as a customer tagged
  "newsletter" plus a tag you choose), an optional image and an optional discount code with a copy
  button. It never shows on first paint: it opens after a delay, when the pointer leaves the top of
  the window (desktop only) and/or after the shopper scrolls part of the page, whichever comes
  first. On screens below 750px it is a small card at the bottom of the screen that leaves the page
  usable, never a full-screen overlay; on desktop a centered modal dialog or a corner card.
  Closes with the close button, Esc or a click outside; a closed offer stays away for the days you
  set, a sign-up for good, and it shows at most once per visit. After a sign-up the page reloads
  with the success message in the popup. Without JavaScript it does not appear at all.
  The discount itself is created in Shopify (Discounts); this section only shows the code.
  Depends on: none
{%- endcomment -%}

{%- liquid
  assign s = section.settings
  assign heading_tag = s.heading_tag | default: 'h2'
  assign form_id = 'lib-popup-form-' | append: section.id
  assign title_id = 'LibPopupTitle-' | append: section.id
  assign input_id = 'LibPopupEmail-' | append: section.id
  assign error_id = 'LibPopupError-' | append: section.id
  assign consent_id = 'LibPopupConsent-' | append: section.id
  assign tags = 'newsletter'
  if s.customer_tag != blank
    assign tags = tags | append: ',' | append: s.customer_tag
  endif
  assign offer_key = s.heading | strip_html | append: '|' | append: s.discount_code
  assign has_image = false
  if s.image != blank
    assign has_image = true
  endif
-%}

<lib-newsletter-popup
  class="lib-popup"
  data-section-id="{{ section.id }}"
  data-form-id="{{ form_id }}"
  data-show="{{ s.show_on }}"
  data-desktop="{{ s.desktop_style }}"
  data-offer="{{ offer_key | escape }}"
  data-days="{{ s.remember_days }}"
  {% if s.trigger_delay %}
    data-delay="{{ s.delay_seconds }}"
  {% endif %}
  {% if s.trigger_exit %}
    data-exit
  {% endif %}
  {% if s.trigger_scroll %}
    data-scroll="{{ s.scroll_percent }}"
  {% 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-backdrop-o: {{ s.backdrop_opacity | divided_by: 100.0 }};
  "
>
  <dialog class="lib-popup__dialog" aria-labelledby="{{ title_id }}" data-has-image="{{ has_image }}">
    <button type="button" class="lib-popup__close" aria-label="{{ s.close_label | default: 'Close' | escape }}">
      <svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" focusable="false"><path d="M6 6l12 12M18 6 6 18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>
    </button>

    {%- if s.image != blank -%}
      <div class="lib-popup__media">
        {{
          s.image
          | image_url: width: 900
          | image_tag: class: 'lib-popup__img', loading: 'lazy', sizes: '360px', widths: '360, 540, 720, 900'
        }}
      </div>
    {%- endif -%}

    <div class="lib-popup__body" data-align="{{ s.text_alignment }}" data-align-m="{{ s.text_alignment_mobile }}">
      <{{ heading_tag }} class="lib-popup__heading lib-popup__heading--{{ s.heading_size }}" id="{{ title_id }}">
        {{- s.heading | default: 'Newsletter' -}}
      </{{ heading_tag }}>
      {%- if s.text != blank -%}
        <div class="lib-popup__text">{{ s.text }}</div>
      {%- endif -%}

      {%- if s.discount_code != blank -%}
        <div class="lib-popup__code">
          <span class="lib-popup__code-label">{{ s.code_label | escape }}</span>
          <span class="lib-popup__code-value">{{ s.discount_code | escape }}</span>
          <button
            type="button"
            class="lib-popup__copy"
            data-code="{{ s.discount_code | escape }}"
            data-copied="{{ s.copied_label | escape }}"
            hidden
          >
            {{- s.copy_label | escape -}}
          </button>
          <span class="lib-popup__sr" role="status" data-copy-status></span>
        </div>
      {%- endif -%}

      {%- form 'customer', id: form_id, class: 'lib-popup__form' -%}
        <input type="hidden" name="contact[tags]" value="{{ tags | escape }}">
        {%- if form.posted_successfully -%}
          <p class="lib-popup__msg lib-popup__msg--success" role="status">{{ s.success_message | escape }}</p>
        {%- else -%}
          {%- if form.errors -%}
            <div class="lib-popup__msg lib-popup__msg--error" id="{{ error_id }}" role="alert">
              {%- 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-popup__label" for="{{ input_id }}">{{ s.input_label | default: 'Email' | escape }}</label>
          <div class="lib-popup__row">
            <input
              class="lib-popup__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.consent != blank %}
                aria-describedby="{% if form.errors %}{{ error_id }} {% endif %}{% if s.consent != blank %}{{ consent_id }}{% endif %}"
              {% endif %}
            >
            <button type="submit" class="lib-popup__submit">{{ s.button_label | default: 'Subscribe' | escape }}</button>
          </div>
          {%- if s.consent != blank -%}
            <p class="lib-popup__consent" id="{{ consent_id }}">{{ s.consent }}</p>
          {%- endif -%}
        {%- endif -%}
      {%- endform -%}
    </div>
  </dialog>
</lib-newsletter-popup>

{% stylesheet %}
  .lib-popup { display: contents; }
  .lib-popup__dialog {
    box-sizing: border-box;
    width: min(100vw - 2rem, 46rem);
    max-height: calc(100vh - 2rem);
    margin: auto;
    padding: 0;
    border: 0;
    border-radius: 16px;
    overflow: auto;
    background-color: var(--lib-bg);
    background-image: var(--lib-bg-image, none);
    color: var(--lib-text);
    box-shadow: 0 30px 80px -30px rgb(0 0 0 / 0.45);
  }
  .lib-popup__dialog[open] { display: grid; grid-template-columns: 1fr; }
  .lib-popup__dialog[open][data-has-image='true'] { grid-template-columns: 2fr 3fr; }
  .lib-popup__dialog::backdrop { background: rgb(12 10 9 / var(--lib-backdrop-o, 0.5)); }
  .lib-popup__dialog:not([data-has-image='true']) { width: min(100vw - 2rem, 30rem); }

  /* Non-modal cards: the desktop corner card and, below 750px, the bottom card. */
  .lib-popup__dialog[data-mode='corner'],
  .lib-popup__dialog[data-mode='sheet'] {
    position: fixed;
    z-index: 2147483000;
    inset: auto 1.25rem 1.25rem auto;
    margin: 0;
    width: min(100vw - 2.5rem, 24rem);
  }
  .lib-popup__dialog[data-mode='corner'][open],
  .lib-popup__dialog[data-mode='sheet'][open] { grid-template-columns: 1fr; }
  .lib-popup__dialog[data-mode='corner'] .lib-popup__media,
  .lib-popup__dialog[data-mode='sheet'] .lib-popup__media { display: none; }
  .lib-popup__dialog[data-mode='sheet'] {
    inset: auto 0.75rem 0.75rem 0.75rem;
    width: auto;
    max-height: 45vh;
    border-radius: 14px;
  }
  .lib-popup__dialog[data-mode='sheet'] .lib-popup__body { padding: 1.125rem 1.125rem 1rem; }
  .lib-popup__dialog[data-mode='sheet'] .lib-popup__text { font-size: 0.9375rem; margin-top: 0.375rem; }
  .lib-popup__dialog[data-mode='sheet'] .lib-popup__heading { font-size: 1.25rem; padding-right: 2.5rem; }

  .lib-popup__media { position: relative; min-height: 100%; background: color-mix(in srgb, var(--lib-text) 8%, transparent); }
  .lib-popup__img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
  .lib-popup__body { display: flex; flex-direction: column; align-items: flex-start; text-align: left; padding: clamp(1.5rem, 4vw, 2.5rem); }
  .lib-popup__body[data-align='center'] { align-items: center; text-align: center; }
  .lib-popup__body[data-align='right'] { align-items: flex-end; text-align: right; }
  @media (max-width: 749px) {
    .lib-popup__body[data-align-m='left'] { align-items: flex-start; text-align: left; }
    .lib-popup__body[data-align-m='center'] { align-items: center; text-align: center; }
    .lib-popup__body[data-align-m='right'] { align-items: flex-end; text-align: right; }
  }
  .lib-popup__close {
    position: absolute;
    top: 0.625rem;
    right: 0.625rem;
    z-index: 1;
    display: grid;
    place-items: center;
    width: 2.5rem;
    height: 2.5rem;
    border: 0;
    border-radius: 50%;
    background: color-mix(in srgb, var(--lib-bg) 85%, transparent);
    color: var(--lib-text);
    cursor: pointer;
  }
  .lib-popup__close:hover { background: color-mix(in srgb, var(--lib-text) 10%, var(--lib-bg)); }
  .lib-popup__close:focus-visible,
  .lib-popup__copy:focus-visible,
  .lib-popup__submit:focus-visible { outline: 2px solid var(--lib-accent); outline-offset: 2px; }

  .lib-popup__heading { margin: 0; padding-right: 2rem; font-size: clamp(1.5rem, 2.6vw, 2rem); line-height: 1.15; letter-spacing: -0.02em; text-wrap: balance; }
  .lib-popup__heading--small { font-size: 1.25rem; }
  .lib-popup__heading--large { font-size: clamp(1.75rem, 3.2vw, 2.5rem); }
  .lib-popup__text { margin-top: 0.75rem; font-size: 1rem; line-height: 1.55; opacity: 0.85; }
  .lib-popup__text > :first-child { margin-top: 0; }
  .lib-popup__text > :last-child { margin-bottom: 0; }

  .lib-popup__code {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 0.75rem;
    margin-top: 1rem;
    padding: 0.625rem 0.875rem;
    border: 1.5px dashed color-mix(in srgb, var(--lib-text) 40%, transparent);
    border-radius: 10px;
  }
  .lib-popup__code-label { font-size: 0.875rem; opacity: 0.8; }
  .lib-popup__code-value { font-weight: 700; letter-spacing: 0.08em; }
  .lib-popup__copy {
    min-height: 2rem;
    padding: 0.25rem 0.75rem;
    border: 1.5px solid currentColor;
    border-radius: 999px;
    background: transparent;
    color: inherit;
    font: inherit;
    font-size: 0.875rem;
    font-weight: 600;
    cursor: pointer;
  }

  .lib-popup__form { width: 100%; margin-top: 1.125rem; }
  .lib-popup__label { display: block; margin-bottom: 0.375rem; font-size: 0.875rem; font-weight: 600; text-align: left; }
  .lib-popup__row { display: flex; gap: 0.5rem; }
  .lib-popup__input {
    flex: 1;
    min-width: 0;
    min-height: 2.875rem;
    padding: 0 0.875rem;
    border: 1.5px solid color-mix(in srgb, var(--lib-text) 35%, transparent);
    border-radius: 999px;
    background: #fff;
    color: #1c1917;
    font: inherit;
  }
  .lib-popup__input:focus-visible { outline: 2px solid var(--lib-accent); outline-offset: 1px; }
  .lib-popup__submit {
    flex: none;
    min-height: 2.875rem;
    padding: 0 1.25rem;
    border: 0;
    border-radius: 999px;
    background: var(--lib-accent);
    color: var(--lib-accent-text);
    font: inherit;
    font-weight: 600;
    cursor: pointer;
  }
  .lib-popup__consent { margin: 0.75rem 0 0; font-size: 0.8125rem; line-height: 1.45; opacity: 0.8; text-align: left; }
  .lib-popup__consent a { color: inherit; text-underline-offset: 0.2em; }
  .lib-popup__msg { margin: 0 0 0.75rem; font-weight: 600; text-align: left; }
  .lib-popup__msg p { margin: 0; }
  .lib-popup__msg--success { margin: 0; padding: 0.75rem 1rem; border-radius: 10px; background: color-mix(in srgb, var(--lib-accent) 12%, transparent); }
  .lib-popup__msg--error { color: #b42318; }
  .lib-popup__sr {
    position: absolute !important;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }

  @media (prefers-reduced-motion: no-preference) {
    .lib-popup[data-animate] .lib-popup__dialog[open] { animation: lib-popup-in 0.35s ease-out; }
    .lib-popup[data-animate] .lib-popup__dialog[data-mode='sheet'][open],
    .lib-popup[data-animate] .lib-popup__dialog[data-mode='corner'][open] { animation-name: lib-popup-up; }
  }
  @keyframes lib-popup-in {
    from { opacity: 0; transform: scale(0.97); }
  }
  @keyframes lib-popup-up {
    from { opacity: 0; transform: translateY(16px); }
  }
{% endstylesheet %}

{% javascript %}
  if (!customElements.get('lib-newsletter-popup')) {
    customElements.define(
      'lib-newsletter-popup',
      class extends HTMLElement {
        connectedCallback() {
          if (this._ready) return;
          this.dialog = this.querySelector('dialog');
          if (!this.dialog || typeof this.dialog.showModal !== 'function') return;
          this._ready = true;
          this.storeKey = `lib-popup:${this.dataset.sectionId}`;
          this.cleanup = [];
          this.listen(this.dialog.querySelector('.lib-popup__close'), 'click', () => this.close(true));
          this.listen(this.dialog, 'cancel', (event) => {
            event.preventDefault();
            this.close(true);
          });
          // A click on the backdrop lands on the dialog itself.
          this.listen(this.dialog, 'click', (event) => {
            if (event.target === this.dialog) this.close(true);
          });
          // Non-modal cards don't get Esc from the browser.
          this.listen(document, 'keydown', (event) => {
            if (event.key === 'Escape' && this.dialog.open && this.dialog.dataset.mode !== 'modal') this.close(true);
          });
          this.setupCopy();

          // Shopify renders the success message only after a successful post.
          if (this.dialog.querySelector('.lib-popup__msg--success')) this.setAttribute('data-posted', '');
          if (window.Shopify && window.Shopify.designMode) {
            this.setupEditor();
            return;
          }
          // Back from a sign-up (or a form error): show the result right away.
          if (location.hash === `#${this.dataset.formId}` && (this.hasAttribute('data-posted') || this.dialog.querySelector('[role="alert"]'))) {
            if (this.hasAttribute('data-posted')) this.remember('signed-up', 365);
            this.open();
            return;
          }
          if (this.isSuppressed()) return;
          this.setupTriggers();
        }

        disconnectedCallback() {
          (this.cleanup || []).forEach((undo) => undo());
          this.cleanup = [];
          clearTimeout(this.timer);
          this._ready = false;
        }

        listen(target, type, handler, options) {
          if (!target) return;
          target.addEventListener(type, handler, options);
          this.cleanup.push(() => target.removeEventListener(type, handler, options));
        }

        storage(kind) {
          try {
            return kind === 'session' ? window.sessionStorage : window.localStorage;
          } catch (error) {
            return null;
          }
        }

        isSuppressed() {
          if (this.dataset.show === 'desktop' && this.isSmall()) return true;
          if (this.dataset.show === 'mobile' && !this.isSmall()) return true;
          const session = this.storage('session');
          if (session && session.getItem(this.storeKey)) return true;
          const local = this.storage('local');
          if (!local) return false;
          try {
            const saved = JSON.parse(local.getItem(this.storeKey) || 'null');
            if (!saved) return false;
            if (saved.state === 'signed-up') return saved.until > Date.now();
            return saved.offer === this.dataset.offer && saved.until > Date.now();
          } catch (error) {
            return false;
          }
        }

        remember(state, days) {
          const local = this.storage('local');
          if (!local) return;
          const until = Date.now() + days * 24 * 60 * 60 * 1000;
          try {
            local.setItem(this.storeKey, JSON.stringify({ state, offer: this.dataset.offer, until }));
          } catch (error) {
            /* Storage full or blocked: the popup may show again, nothing else breaks. */
          }
        }

        setupTriggers() {
          const fire = () => this.open();
          const delay = Number(this.dataset.delay);
          if (delay > 0) this.timer = setTimeout(fire, delay * 1000);
          if (this.hasAttribute('data-exit') && window.matchMedia('(hover: hover) and (pointer: fine)').matches) {
            this.listen(document, 'mouseout', (event) => {
              if (!event.relatedTarget && event.clientY <= 0) fire();
            });
          }
          const share = Number(this.dataset.scroll);
          if (share > 0) {
            this.listen(
              window,
              'scroll',
              () => {
                const room = document.documentElement.scrollHeight - window.innerHeight;
                if (room > 0 && (window.scrollY / room) * 100 >= share) fire();
              },
              { passive: true }
            );
          }
        }

        setupEditor() {
          const id = this.dataset.sectionId;
          this.listen(document, 'shopify:section:select', (event) => {
            if (event.detail && event.detail.sectionId === id) this.open();
          });
          this.listen(document, 'shopify:section:deselect', (event) => {
            if (event.detail && event.detail.sectionId === id) this.close(false);
          });
        }

        setupCopy() {
          const button = this.querySelector('.lib-popup__copy');
          if (!button || !navigator.clipboard) return;
          button.hidden = false;
          const status = this.querySelector('[data-copy-status]');
          this.listen(button, 'click', () => {
            navigator.clipboard.writeText(button.dataset.code).then(
              () => {
                if (status) status.textContent = button.dataset.copied;
              },
              () => {
                /* Clipboard refused: the code stays visible to copy by hand. */
              }
            );
          });
        }

        isSmall() {
          return window.matchMedia('(max-width: 749px)').matches;
        }

        open() {
          if (this.dialog.open) return;
          clearTimeout(this.timer);
          const session = this.storage('session');
          if (session) {
            try {
              session.setItem(this.storeKey, '1');
            } catch (error) {
              /* Blocked storage only means it may show again on the next page. */
            }
          }
          this.returnFocus = document.activeElement;
          const mode = this.isSmall() ? 'sheet' : this.dataset.desktop === 'corner' ? 'corner' : 'modal';
          this.dialog.dataset.mode = mode;
          if (mode === 'modal') this.dialog.showModal();
          else this.dialog.show();
        }

        close(remember) {
          if (!this.dialog.open) return;
          this.dialog.close();
          if (remember && !this.hasAttribute('data-posted')) this.remember('closed', Number(this.dataset.days) || 1);
          if (this.returnFocus && this.returnFocus.isConnected && typeof this.returnFocus.focus === 'function') this.returnFocus.focus();
          this.returnFocus = null;
        }
      }
    );
  }
{% endjavascript %}

{% schema %}
{
  "name": "Lib · Newsletter popup",
  "tag": "section",
  "class": "lib-section",
  "settings": [
    { "type": "paragraph", "content": "Opens after a delay, on exit intent or after scrolling, never on page load. Below 750px it is a small card at the bottom of the screen. In the editor it shows while this section is selected." },
    { "type": "header", "content": "Offer" },
    { "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "10% off your first order" },
    {
      "type": "select",
      "id": "heading_tag",
      "label": "Heading tag",
      "options": [
        { "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>Join the studio letter for new pieces and kiln days. One email a month.</p>" },
    { "type": "image_picker", "id": "image", "label": "Image", "info": "Optional. Desktop modal only; the small cards leave it out." },
    { "type": "text", "id": "discount_code", "label": "Discount code", "info": "Optional. Create the discount in Shopify under Discounts; this only shows the code." },
    { "type": "text", "id": "code_label", "label": "Code label", "default": "Use code" },
    { "type": "text", "id": "copy_label", "label": "Copy button", "default": "Copy" },
    { "type": "text", "id": "copied_label", "label": "Copied message", "info": "Read by screen readers after copying.", "default": "Code copied" },

    { "type": "header", "content": "Form" },
    { "type": "text", "id": "input_label", "label": "Field label", "default": "Email address" },
    { "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": "Added next to \"newsletter\", so you can tell popup sign-ups apart.", "default": "popup" },
    { "type": "inline_richtext", "id": "consent", "label": "Consent text", "default": "Unsubscribe any time. See our <a href=\"/policies/privacy-policy\">privacy policy</a>." },
    { "type": "text", "id": "success_message", "label": "Success message", "default": "Thanks for joining. Check your inbox for the code." },
    { "type": "text", "id": "close_label", "label": "Close button label", "default": "Close" },

    { "type": "header", "content": "When it opens" },
    { "type": "checkbox", "id": "trigger_delay", "label": "After a delay", "default": true },
    { "type": "range", "id": "delay_seconds", "label": "Delay", "min": 10, "max": 120, "step": 5, "unit": "s", "default": 20, "visible_if": "{{ section.settings.trigger_delay }}" },
    { "type": "checkbox", "id": "trigger_exit", "label": "When the pointer leaves the window", "info": "Desktop only.", "default": true },
    { "type": "checkbox", "id": "trigger_scroll", "label": "After scrolling", "default": false },
    { "type": "range", "id": "scroll_percent", "label": "Scrolled share of the page", "min": 20, "max": 90, "step": 5, "unit": "%", "default": 50, "visible_if": "{{ section.settings.trigger_scroll }}" },
    { "type": "range", "id": "remember_days", "label": "Stay closed for", "info": "After a shopper closes it. A sign-up hides it for a year.", "min": 1, "max": 90, "step": 1, "unit": "d", "default": 14 },

    { "type": "header", "content": "Layout" },
    {
      "type": "select",
      "id": "desktop_style",
      "label": "On desktop",
      "options": [
        { "value": "modal", "label": "Centered dialog" },
        { "value": "corner", "label": "Corner card" }
      ],
      "default": "modal"
    },
    { "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": "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": "header", "content": "Colors" },
    { "type": "color", "id": "color_bg", "label": "Background", "default": "#FFFDF9" },
    { "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 (button, focus ring)", "default": "#2A2420" },
    { "type": "color", "id": "color_accent_text", "label": "Text on accent", "default": "#FFFDF9" },
    { "type": "range", "id": "backdrop_opacity", "label": "Page dimming behind the dialog", "min": 0, "max": 80, "step": 5, "unit": "%", "default": 45 },

    { "type": "header", "content": "Motion" },
    { "type": "checkbox", "id": "animate", "label": "Fade in", "info": "Never when reduced motion is on.", "default": true }
  ],
  "presets": [
    { "name": "Lib · Newsletter popup" }
  ]
}
{% endschema %}
```

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