# Sticky promo bar: free Shopify section

> A sticky announcement bar with a discount code copy button, a start and end time and a close button that's remembered. Fixed bottom or pinned top.

Sticky promo bar is a free Shopify section for a short offer that stays on screen: a message, an optional discount code with a copy button and a link, fixed to the bottom of the screen or pinned to the top once the shopper scrolls past it. It can run between a start and end time in the store's time zone, checked by Liquid and again in the browser so a cached page never shows an ended offer, and a close button hides it for the number of days you choose. The code is text you type: the discount is created in Shopify. It works in Online Store 2.0 themes without an app.

- **Category:** Conversion and offers
- **Use on:** Every page, Home, Collection
- **Reads:** Content only
- **Size:** 21 KB in 2 files, with a small script
- **License:** MIT, © 2026 Lintel
- **Page:** https://lintelagency.com/resources/shopify-sections/sticky-promo-bar
- **Live demo:** https://lintelagency.com/resources/shopify-sections/demo/lib-sticky-promo-bar.html
- **Added:** 2026-11-02

## What it’s for

- A weekend sale with a discount code shoppers can copy in one tap
- A launch or holiday message that starts and ends on its own at set times
- A bottom bar on mobile with a link to the sale collection

## Editor settings

In the theme editor the section is called “Lib · Sticky promo bar”. 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 |
| --- | --- | --- | --- | --- |
| `message` | `inline_richtext` | Message | `<strong>Autumn sale:</strong> 15% off everything until Sunday` |  |
| `code` | `text` | Discount code | – |  |
| `code_label` | `text` | Text before the code | Use code |  |
| `copy_label` | `text` | Copy button | Copy |  |
| `copied_label` | `text` | Copied message | Code copied |  |
| `link_label` | `text` | Link text | – |  |
| `link_url` | `url` | Link | – |  |
| `link_style` | `select` | Link style | text | `primary`, `outline`, `text` |
| `link_new_tab` | `checkbox` | Open link in a new tab | `false` |  |
| `region_label` | `text` | Bar name (screen readers) | Current offer |  |

### Position and schedule

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `position` | `select` | Position | bottom | `bottom`, `top` |
| `top_offset` | `range` | Distance from the top when pinned | `0` | 0–200 px, step 2 |
| `starts_at` | `text` | Show from | – |  |
| `ends_at` | `text` | Show until | – |  |
| `dismissible` | `checkbox` | Show a close button | `true` |  |
| `close_label` | `text` | Close button label (screen readers) | Close offer |  |
| `remember_days` | `range` | Keep closed for | `7` | 0–30 d, step 1 |

### Layout

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `section_width` | `select` | Width | page | `page`, `full` |
| `text_alignment` | `select` | Alignment on desktop | center | `left`, `center` |
| `text_alignment_mobile` | `select` | Alignment on mobile | left | `left`, `center` |

### Colors

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `color_bg` | `color` | Background | #2B2620 |  |
| `color_bg_gradient` | `color_background` | Background gradient | – |  |
| `color_text` | `color` | Text | #FAF6F1 |  |
| `color_accent` | `color` | Copy button | #E9C99A |  |
| `color_accent_text` | `color` | Copy button text | #2B2620 |  |

### Spacing and visibility

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `padding_top` | `range` | Top padding on desktop | `10` | 0–40 px, step 2 |
| `padding_bottom` | `range` | Bottom padding on desktop | `10` | 0–40 px, step 2 |
| `padding_top_mobile` | `range` | Top padding on mobile | `8` | 0–40 px, step 2 |
| `padding_bottom_mobile` | `range` | Bottom padding on mobile | `8` | 0–40 px, step 2 |
| `show_on` | `select` | Show on | all | `all`, `desktop`, `mobile` |
| `anchor_id` | `text` | Anchor ID | – |  |

### Motion

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `animate` | `checkbox` | Slide the bar 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-sticky-promo-bar.liquid` and paste the contents of `sections/lib-sticky-promo-bar.liquid` from the source below. Save.
4. In the `snippets` folder, add `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 · Sticky promo bar**. 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-sticky-promo-bar.liquid https://lintelagency.com/resources/shopify-sections/files/lib-sticky-promo-bar.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-sticky-promo-bar.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-sticky-promo-bar.liquid](https://lintelagency.com/resources/shopify-sections/files/lib-sticky-promo-bar.liquid): goes in `sections/`
- [snippets/lib-button.liquid](https://lintelagency.com/resources/shopify-sections/files/lib-button.liquid): goes in `snippets/`

## Questions

### Does it create the discount code?

No. Create the discount under Discounts in the Shopify admin, then type the same code in the section. The copy button copies exactly what you typed.

### How does the schedule work?

Type a start and end as YYYY-MM-DD HH:MM in the store's time zone. Liquid leaves the bar out outside that window, and the script checks the time again in the browser, so a page served from a cache doesn't show the bar after it ends. Leave both empty to show it always.

### How long does it stay closed after a shopper closes it?

For the number of days you set (0–30), stored in the shopper's browser. With 0 it stays closed until the tab is closed. Changing the message or code shows the new offer again, even to shoppers who closed the old one.

### Will it cover my sticky header?

Pinned to the top, the bar sits at the distance you set, so use your sticky header's height. Fixed to the bottom, the section adds matching space at the end of the page so it never hides the footer.

### Is it accessible?

The bar is a labelled region, the copy and close buttons are real buttons with visible focus and at least 32 px tall, "Code copied" is announced to screen readers, and the slide-in is off when reduced motion is on.

## Source

### sections/lib-sticky-promo-bar.liquid

```liquid
{%- comment -%}
  MIT License · (c) 2026 Lintel · https://lintelagency.com/resources/shopify-sections
{%- endcomment -%}
{%- comment -%}
  Sticky promo bar: a short offer with an optional discount code and copy button, fixed to the
  bottom of the screen or pinned to the top once the shopper scrolls past it. It can run on a
  schedule (start and end in the store's time zone, checked by Liquid and again in the browser,
  so a cached page doesn't show an ended offer) and can be dismissed; the dismissal is remembered
  in the browser for the days you choose. The code is text you type: create the discount itself
  in Shopify.
  Depends on: snippets/lib-button.liquid
{%- endcomment -%}

{%- liquid
  assign s = section.settings
  assign now = 'now' | date: '%Y-%m-%d %H:%M'
  assign starts = s.starts_at | strip
  assign ends = s.ends_at | strip
  assign in_window = true
  if starts != blank and now < starts
    assign in_window = false
  endif
  if ends != blank and now >= ends
    assign in_window = false
  endif
  assign tz = 'now' | date: '%z'
  assign code_id = 'LibPromoCode-' | append: section.id
-%}

{%- if in_window or request.design_mode -%}
  <lib-sticky-promo-bar
    class="lib-promo lib-promo--{{ s.section_width }}"
    {% if s.anchor_id != blank %}
      id="{{ s.anchor_id | handleize }}"
    {% endif %}
    data-show="{{ s.show_on }}"
    data-position="{{ s.position }}"
    data-starts="{{ starts | escape }}"
    data-ends="{{ ends | escape }}"
    data-tz="{{ tz }}"
    data-remember="{{ s.remember_days }}"
    {% if request.design_mode %}
      data-editor
    {% 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-offset: {{ s.top_offset }}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-promo__spacer" data-spacer></div>
    <aside class="lib-promo__bar" aria-label="{{ s.region_label | escape }}" data-bar>
      <div class="lib-promo__inner" data-align="{{ s.text_alignment }}" data-align-m="{{ s.text_alignment_mobile }}">
        <div class="lib-promo__content">
          {%- if s.message != blank -%}
            <p class="lib-promo__message">{{ s.message }}</p>
          {%- endif -%}
          {%- if s.code != blank -%}
            <span class="lib-promo__code-wrap">
              <span class="lib-promo__code-label">{{ s.code_label | escape }}</span>
              <code class="lib-promo__code" id="{{ code_id }}">{{ s.code | escape }}</code>
              {%- if s.copy_label != blank -%}
                <button type="button" class="lib-promo__copy" aria-describedby="{{ code_id }}" data-copy>
                  {{- s.copy_label | escape -}}
                </button>
              {%- endif -%}
            </span>
          {%- endif -%}
          {%- if s.link_label != blank -%}
            <span class="lib-promo__link">
              {%- render 'lib-button', label: s.link_label, url: s.link_url, style: s.link_style, new_tab: s.link_new_tab, arrow: false -%}
            </span>
          {%- endif -%}
        </div>
        <span class="lib-promo__status" role="status" data-status></span>
        {%- if s.dismissible -%}
          <button type="button" class="lib-promo__close" aria-label="{{ s.close_label | escape }}" data-close>
            <svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false"><path d="M4 4l8 8M12 4l-8 8" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>
          </button>
        {%- endif -%}
      </div>
    </aside>
    <template data-copied>{{ s.copied_label | escape }}</template>
  </lib-sticky-promo-bar>
{%- endif -%}

{% stylesheet %}
  .lib-promo { display: block; }
  .lib-promo[hidden] { display: none; }
  .lib-promo__bar {
    z-index: 40;
    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-promo[data-position='bottom'] .lib-promo__bar {
    position: fixed;
    inset: auto 0 0;
    box-shadow: 0 -4px 18px rgb(0 0 0 / 0.08);
  }
  .lib-promo[data-position='top'].is-stuck .lib-promo__bar {
    position: fixed;
    inset: var(--lib-offset) 0 auto;
    box-shadow: 0 4px 18px rgb(0 0 0 / 0.08);
  }
  .lib-promo__inner {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    max-width: 1200px;
    margin: 0 auto;
  }
  .lib-promo--full .lib-promo__inner { max-width: none; }
  @media (max-width: 749px) {
    .lib-promo__bar { padding-top: var(--lib-pt-m); padding-bottom: var(--lib-pb-m); }
    .lib-promo[data-show='desktop'] { display: none; }
  }
  @media (min-width: 750px) {
    .lib-promo[data-show='mobile'] { display: none; }
  }

  .lib-promo__content {
    display: flex;
    flex: 1;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 1rem;
    min-width: 0;
    font-size: 0.9375rem;
    line-height: 1.4;
  }
  .lib-promo__inner[data-align='center'] .lib-promo__content { justify-content: center; text-align: center; }
  @media (max-width: 749px) {
    .lib-promo__inner[data-align-m='left'] .lib-promo__content { justify-content: flex-start; text-align: left; }
    .lib-promo__inner[data-align-m='center'] .lib-promo__content { justify-content: center; text-align: center; }
  }
  .lib-promo__message { margin: 0; }
  .lib-promo__message a { color: inherit; }
  .lib-promo__code-wrap { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; }
  .lib-promo__code-label { font-size: 0.875rem; }
  .lib-promo__code {
    padding: 0.2rem 0.55rem;
    border: 1.5px dashed currentColor;
    border-radius: 6px;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.875rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    user-select: all;
  }
  .lib-promo__copy {
    min-height: 2rem;
    min-width: 2.75rem;
    padding: 0.35rem 0.8rem;
    border: 0;
    border-radius: 999px;
    background: var(--lib-accent);
    color: var(--lib-accent-text);
    font: inherit;
    font-size: 0.8125rem;
    font-weight: 600;
    cursor: pointer;
  }
  .lib-promo__copy:focus-visible,
  .lib-promo__close:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
  .lib-promo__link { font-size: 0.875rem; }
  .lib-promo__link .lib-btn { padding: 0.55em 1.1em; }
  .lib-promo__link .lib-btn--text { padding-inline: 0; color: inherit; }
  .lib-promo__status {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .lib-promo__close {
    flex: none;
    display: grid;
    place-items: center;
    width: 2.25rem;
    height: 2.25rem;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: inherit;
    cursor: pointer;
  }
  .lib-promo__close:hover { background: color-mix(in srgb, currentColor 12%, transparent); }

  @media (prefers-reduced-motion: no-preference) {
    .lib-promo[data-animate][data-position='bottom'] .lib-promo__bar { animation: lib-promo-up 0.45s cubic-bezier(0.2, 0.7, 0.2, 1) both; }
    .lib-promo[data-animate].is-stuck .lib-promo__bar { animation: lib-promo-down 0.35s cubic-bezier(0.2, 0.7, 0.2, 1) both; }
  }
  @keyframes lib-promo-up {
    from { transform: translateY(100%); }
  }
  @keyframes lib-promo-down {
    from { transform: translateY(-100%); }
  }
{% endstylesheet %}

{% javascript %}
  (() => {
    if (customElements.get('lib-sticky-promo-bar')) return;
    const DAY_MS = 86400000;
    const COPIED_RESET_MS = 2500;

    // A short, stable key per offer: a new message or code shows again to shoppers who dismissed the old one.
    const hash = (text) => {
      let h = 5381;
      for (let i = 0; i < text.length; i += 1) h = ((h << 5) + h + text.charCodeAt(i)) | 0;
      return (h >>> 0).toString(36);
    };
    // "2026-11-27 09:00" in the store's time zone ("+0300") -> a Date.
    const parse = (value, tz) => {
      if (!value) return null;
      const offset = /^[+-]\d{4}$/.test(tz) ? `${tz.slice(0, 3)}:${tz.slice(3)}` : 'Z';
      const date = new Date(`${value.trim().replace(' ', 'T')}:00${offset}`);
      return Number.isNaN(date.getTime()) ? null : date;
    };

    class LibStickyPromoBar extends HTMLElement {
      connectedCallback() {
        this.bar = this.querySelector('[data-bar]');
        this.key = `lib-promo-dismissed:${hash(this.bar.textContent.replace(/\s+/g, ' '))}`;
        const editor = this.hasAttribute('data-editor');
        if (!editor && (!this.inWindow() || this.wasDismissed())) {
          this.hidden = true;
          return;
        }
        this.querySelector('[data-close]')?.addEventListener('click', () => this.dismiss());
        this.querySelector('[data-copy]')?.addEventListener('click', (event) => this.copy(event.currentTarget));
        if (this.dataset.position === 'top') this.pinOnScroll();
        if (this.dataset.position === 'bottom') this.reserveSpace();
      }

      disconnectedCallback() {
        this.observer?.disconnect();
        this.resizer?.disconnect();
        document.body.style.removeProperty('padding-bottom');
      }

      inWindow() {
        const now = Date.now();
        const starts = parse(this.dataset.starts, this.dataset.tz);
        const ends = parse(this.dataset.ends, this.dataset.tz);
        return (!starts || now >= starts.getTime()) && (!ends || now < ends.getTime());
      }

      storage() {
        return Number(this.dataset.remember) > 0 ? localStorage : sessionStorage;
      }

      wasDismissed() {
        try {
          const until = Number(this.storage().getItem(this.key));
          return until > Date.now();
        } catch (error) {
          // Storage blocked: show the bar; dismissing still works for this page view.
          return false;
        }
      }

      dismiss() {
        const days = Number(this.dataset.remember);
        const until = Date.now() + (days > 0 ? days * DAY_MS : DAY_MS);
        try {
          this.storage().setItem(this.key, String(until));
        } catch (error) {
          console.warn('Promo bar: the dismissal could not be remembered.', error);
        }
        this.hidden = true;
        this.disconnectedCallback();
      }

      async copy(button) {
        const code = this.querySelector('.lib-promo__code');
        const status = this.querySelector('[data-status]');
        const copied = this.querySelector('[data-copied]').content.textContent.trim();
        try {
          await navigator.clipboard.writeText(code.textContent.trim());
          status.textContent = copied;
          const label = button.textContent;
          button.textContent = copied;
          setTimeout(() => {
            button.textContent = label;
            status.textContent = '';
          }, COPIED_RESET_MS);
        } catch (error) {
          // No clipboard access (old browser, insecure page): select the code so it can be copied by hand.
          const range = document.createRange();
          range.selectNodeContents(code);
          const selection = window.getSelection();
          selection.removeAllRanges();
          selection.addRange(range);
        }
      }

      // Top: the bar sits in its place and stays at the top once its place scrolls out of view.
      pinOnScroll() {
        const spacer = this.querySelector('[data-spacer]');
        this.observer = new IntersectionObserver(([entry]) => {
          const stuck = !entry.isIntersecting && entry.boundingClientRect.top < 0;
          spacer.style.height = stuck ? `${this.bar.offsetHeight}px` : '';
          this.classList.toggle('is-stuck', stuck);
        });
        this.observer.observe(spacer);
      }

      // Bottom: keep the page's last lines clear of the fixed bar.
      reserveSpace() {
        this.resizer = new ResizeObserver(() => {
          document.body.style.paddingBottom = `${this.bar.offsetHeight}px`;
        });
        this.resizer.observe(this.bar);
      }
    }
    customElements.define('lib-sticky-promo-bar', LibStickyPromoBar);
  })();
{% endjavascript %}

{% schema %}
{
  "name": "Lib · Sticky promo bar",
  "tag": "section",
  "class": "lib-section",
  "settings": [
    { "type": "header", "content": "Content" },
    { "type": "inline_richtext", "id": "message", "label": "Message", "default": "<strong>Autumn sale:</strong> 15% off everything until Sunday" },
    { "type": "text", "id": "code", "label": "Discount code", "info": "Optional. Create the discount itself in Shopify under Discounts." },
    { "type": "text", "id": "code_label", "label": "Text before the code", "default": "Use code" },
    { "type": "text", "id": "copy_label", "label": "Copy button", "info": "Leave empty to hide the button.", "default": "Copy" },
    { "type": "text", "id": "copied_label", "label": "Copied message", "default": "Code copied" },
    { "type": "text", "id": "link_label", "label": "Link text" },
    { "type": "url", "id": "link_url", "label": "Link" },
    {
      "type": "select",
      "id": "link_style",
      "label": "Link style",
      "options": [
        { "value": "primary", "label": "Solid" },
        { "value": "outline", "label": "Outline" },
        { "value": "text", "label": "Text link" }
      ],
      "default": "text"
    },
    { "type": "checkbox", "id": "link_new_tab", "label": "Open link in a new tab", "default": false },
    { "type": "text", "id": "region_label", "label": "Bar name (screen readers)", "default": "Current offer" },

    { "type": "header", "content": "Position and schedule" },
    {
      "type": "select",
      "id": "position",
      "label": "Position",
      "options": [
        { "value": "bottom", "label": "Fixed to the bottom of the screen" },
        { "value": "top", "label": "In place, then pinned to the top on scroll" }
      ],
      "default": "bottom"
    },
    { "type": "range", "id": "top_offset", "label": "Distance from the top when pinned", "info": "Use your sticky header's height so the bar sits below it.", "min": 0, "max": 200, "step": 2, "unit": "px", "default": 0, "visible_if": "{{ section.settings.position == 'top' }}" },
    { "type": "text", "id": "starts_at", "label": "Show from", "info": "Optional. YYYY-MM-DD HH:MM in the store's time zone, for example 2026-11-27 09:00." },
    { "type": "text", "id": "ends_at", "label": "Show until", "info": "Optional. Same format. The bar disappears at this time." },
    { "type": "checkbox", "id": "dismissible", "label": "Show a close button", "default": true },
    { "type": "text", "id": "close_label", "label": "Close button label (screen readers)", "default": "Close offer" },
    { "type": "range", "id": "remember_days", "label": "Keep closed for", "info": "0 keeps it closed until the browser tab is closed.", "min": 0, "max": 30, "step": 1, "unit": "d", "default": 7 },

    { "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": "select",
      "id": "text_alignment",
      "label": "Alignment on desktop",
      "options": [
        { "value": "left", "label": "Left" },
        { "value": "center", "label": "Center" }
      ],
      "default": "center"
    },
    {
      "type": "select",
      "id": "text_alignment_mobile",
      "label": "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": "#2B2620" },
    { "type": "color_background", "id": "color_bg_gradient", "label": "Background gradient", "info": "Optional. Covers the background color." },
    { "type": "color", "id": "color_text", "label": "Text", "default": "#FAF6F1" },
    { "type": "color", "id": "color_accent", "label": "Copy button", "default": "#E9C99A" },
    { "type": "color", "id": "color_accent_text", "label": "Copy button text", "default": "#2B2620" },

    { "type": "header", "content": "Spacing and visibility" },
    { "type": "range", "id": "padding_top", "label": "Top padding on desktop", "min": 0, "max": 40, "step": 2, "unit": "px", "default": 10 },
    { "type": "range", "id": "padding_bottom", "label": "Bottom padding on desktop", "min": 0, "max": 40, "step": 2, "unit": "px", "default": 10 },
    { "type": "range", "id": "padding_top_mobile", "label": "Top padding on mobile", "min": 0, "max": 40, "step": 2, "unit": "px", "default": 8 },
    { "type": "range", "id": "padding_bottom_mobile", "label": "Bottom padding on mobile", "min": 0, "max": 40, "step": 2, "unit": "px", "default": 8 },
    {
      "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": "Slide the bar in", "info": "Never when reduced motion is on.", "default": true }
  ],
  "presets": [{ "name": "Lib · Sticky promo bar", "settings": { "code": "AUTUMN15" } }]
}
{% endschema %}
```

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