# Video hero: free Shopify section

> A full-width muted background video with text and buttons on top, a separate mobile video, poster images and a pause button. Loads the video only on screen.

Video hero is a free Shopify section that plays a muted, looping Shopify-hosted video behind a heading, text and up to two buttons, with an optional separate video for mobile. Poster images show first and stay when motion is reduced, a pause button is always there, and the video file only loads when the section is on screen. It works in Online Store 2.0 themes without an app.

- **Category:** Hero and banners
- **Use on:** Home, Collection, Landing pages
- **Reads:** Shopify-hosted video
- **Size:** 34 KB in 3 files, with a small script
- **License:** MIT, © 2026 Lintel
- **Page:** https://lintelagency.com/resources/shopify-sections/video-hero
- **Live demo:** https://lintelagency.com/resources/shopify-sections/demo/lib-video-hero.html
- **Added:** 2026-11-02

## What it’s for

- Home page opening that shows a product being made, worn or used
- Campaign or launch landing page with a short brand film behind the headline
- Collection page header with a looping clip of the range in use

## Editor settings

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

### Video

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `video` | `video` | Desktop video | – |  |
| `video_mobile` | `video` | Mobile video | – |  |
| `image` | `image_picker` | Desktop poster image | – |  |
| `image_mobile` | `image_picker` | Mobile poster image | – |  |
| `pause_label` | `text` | Pause button label | Pause background video |  |
| `play_label` | `text` | Play button label | Play background video |  |
| `overlay_color` | `color` | Overlay color | #1C1917 |  |
| `overlay_opacity` | `range` | Overlay opacity | `35` | 0–90 %, step 5 |

### Content

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `eyebrow` | `text` | Eyebrow | Made in our studio |  |
| `heading` | `inline_richtext` | Heading | `Shaped by hand, <em>fired with care</em>` |  |
| `heading_tag` | `select` | Heading tag | h2 | `h1`, `h2`, `h3`, `p` |
| `heading_size` | `select` | Heading size | large | `small`, `medium`, `large`, `xlarge` |
| `text` | `richtext` | Text | `<p>Small-batch stoneware for everyday tables, glazed in earthy tones and made to last.</p>` |  |

### Layout

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `height` | `select` | Height on desktop | large | `small`, `medium`, `large`, `full` |
| `height_mobile` | `select` | Height on mobile | large | `small`, `medium`, `large`, `full` |
| `section_width` | `select` | Width | full | `page`, `full` |
| `corner_radius` | `range` | Corner radius | `12` | 0–40 px, step 2 |
| `content_position` | `select` | Content position on desktop | middle-left | `top-left`, `top-center`, `top-right`, `middle-left`, `middle-center`, `middle-right`, `bottom-left`, `bottom-center`, `bottom-right` |
| `content_position_mobile` | `select` | Content position on mobile | bottom | `top`, `middle`, `bottom` |
| `text_alignment_mobile` | `select` | Text alignment on mobile | left | `left`, `center`, `right` |
| `content_width` | `range` | Content max width | `620` | 320–960 px, step 20 |
| `content_box` | `select` | Content box | none | `none`, `solid` |

### Colors

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `color_bg` | `color` | Background | #F5EFE6 |  |
| `color_text` | `color` | Text | #FFFFFF |  |
| `color_accent` | `color` | Solid button | #FFFFFF |  |
| `color_accent_text` | `color` | Text on solid button | #1C1917 |  |
| `color_box` | `color` | Content box | #F5EFE6 |  |
| `color_box_text` | `color` | Text in content box | #2A211B |  |

### Spacing and visibility

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

## Blocks

### Button (`button`, up to 2)

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `label` | `text` | Label | Shop new pieces |  |
| `url` | `url` | Link | – |  |
| `style` | `select` | Style | primary | `primary`, `outline`, `text` |
| `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-video-hero.liquid` and paste the contents of `sections/lib-video-hero.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 · Video hero**. 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-video-hero.liquid https://lintelagency.com/resources/shopify-sections/files/lib-video-hero.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-video-hero.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-video-hero.liquid](https://lintelagency.com/resources/shopify-sections/files/lib-video-hero.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

### Where does the video come from?

You upload it to Shopify in the section's video setting, so it is hosted by Shopify; no YouTube or Vimeo player loads. The section picks the largest MP4 Shopify made from your upload that is no wider than the screen needs: up to 1920px on desktop and 1080px for the mobile video.

### Can I use a different video on mobile?

Yes. Add a mobile video and it plays below 750px instead of the desktop one, which is then never downloaded on phones. Without a mobile video the desktop video plays everywhere. Desktop and mobile poster images are set separately too, each with its own focal point.

### Is it accessible?

The video is muted, decorative and hidden from screen readers. A pause and play button sits in the corner whenever a video can play, with labels you can change in the editor. Visitors whose device asks for reduced motion see the poster image and can start the video with the play button.

### Will it slow my page down?

The poster image loads like any hero image (eagerly when the section is near the top of the page). The video file is only requested once the section is on screen, it pauses when scrolled away or when the browser tab is hidden, and the script is a small custom element with no library.

### What happens without JavaScript or when autoplay is blocked?

The poster image stays, with the text and buttons on top. If a browser refuses to autoplay (for example in data saver mode), the button switches to play so the visitor can start it.

## Source

### sections/lib-video-hero.liquid

```liquid
{%- comment -%}
  MIT License · (c) 2026 Lintel · https://lintelagency.com/resources/shopify-sections
{%- endcomment -%}
{%- comment -%}
  Video hero: a full-bleed, muted, looping Shopify-hosted background video (with an optional
  separate mobile video) under an overlay, with eyebrow, heading, text and up to two buttons (blocks) on top.
  Desktop and mobile poster images (each with its focal point) show first, and stay when there
  is no JavaScript, no video, or the visitor prefers reduced motion. A pause / play button is
  always shown while a video can play. The video file only loads when the section is on screen,
  and it pauses when scrolled away or when the tab is hidden.
  Depends on: snippets/lib-image.liquid, snippets/lib-button.liquid
{%- endcomment -%}

{%- liquid
  assign s = section.settings
  assign heading_tag = s.heading_tag | default: 'h2'
  assign eager = false
  if section.index <= 2
    assign eager = true
  endif

  comment
    Which video plays where: one video for every device, or a desktop and a mobile one.
  endcomment
  assign desktop_device = 'all'
  if s.video_mobile != blank
    assign desktop_device = 'desktop'
  endif
  assign has_video = false
  if s.video != blank or s.video_mobile != blank
    assign has_video = true
  endif
-%}

<lib-video-hero
  class="lib-video-hero lib-video-hero--{{ s.section_width }}"
  {% if s.anchor_id != blank %}
    id="{{ s.anchor_id | handleize }}"
  {% endif %}
  data-show="{{ s.show_on }}"
  data-height="{{ s.height }}"
  data-height-m="{{ s.height_mobile }}"
  {% if s.animate %}
    data-animate
  {% endif %}
  style="
    --lib-bg: {{ s.color_bg }};
    --lib-text: {{ s.color_text }};
    --lib-accent: {{ s.color_accent }};
    --lib-accent-text: {{ s.color_accent_text }};
    --lib-box: {{ s.color_box }};
    --lib-box-text: {{ s.color_box_text }};
    --lib-overlay: {{ s.overlay_color }};
    --lib-overlay-o: {{ s.overlay_opacity | divided_by: 100.0 }};
    --lib-content-w: {{ s.content_width }}px;
    --lib-radius-frame: {{ s.corner_radius }}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-video-hero__frame">
    <div class="lib-video-hero__media">
      {%- render 'lib-image',
        image: s.image,
        image_mobile: s.image_mobile,
        class: 'lib-video-hero__img',
        sizes: '100vw',
        eager: eager,
        placeholder: 'hero-apparel-1'
      -%}

      {%- for device in (1..2) -%}
        {%- liquid
          if device == 1
            assign video = s.video
            assign max_width = 1920
            assign device_name = desktop_device
            assign media = '(min-width: 750px)'
            if desktop_device == 'all'
              assign media = 'all'
            endif
          else
            assign video = s.video_mobile
            assign max_width = 1080
            assign device_name = 'mobile'
            assign media = '(max-width: 749px)'
          endif

          comment
            Shopify transcodes every upload into several MP4 sizes (and HLS); take the largest MP4
            that is still no wider than the device needs.
          endcomment
          assign source_url = ''
          assign source_width = 0
          if video != blank
            for source in video.sources
              if source.mime_type == 'video/mp4' and source.width <= max_width and source.width > source_width
                assign source_url = source.url
                assign source_width = source.width
              endif
            endfor
            if source_url == blank
              for source in video.sources
                if source.mime_type == 'video/mp4'
                  assign source_url = source.url
                  break
                endif
              endfor
            endif
          endif
        -%}
        {%- if source_url != blank -%}
          <video
            class="lib-video-hero__video"
            data-device="{{ device_name }}"
            data-media="{{ media }}"
            muted
            loop
            playsinline
            disablepictureinpicture
            preload="none"
            aria-hidden="true"
            tabindex="-1"
          >
            <source data-src="{{ source_url }}" type="video/mp4">
          </video>
        {%- endif -%}
      {%- endfor -%}
    </div>
    <div class="lib-video-hero__overlay" aria-hidden="true"></div>

    <div
      class="lib-video-hero__content"
      data-pos="{{ s.content_position }}"
      data-pos-m="{{ s.content_position_mobile }}"
      data-align-m="{{ s.text_alignment_mobile }}"
    >
      <div
        class="lib-video-hero__box"
        data-box="{{ s.content_box }}"
        data-align="{{ s.content_position | split: '-' | last }}"
        data-align-m="{{ s.text_alignment_mobile }}"
      >
        {%- if s.eyebrow != blank -%}
          <p class="lib-video-hero__eyebrow">{{ s.eyebrow | escape }}</p>
        {%- endif -%}

        {%- if s.heading != blank -%}
          <{{ heading_tag }} class="lib-video-hero__heading lib-video-hero__heading--{{ s.heading_size }}">
            {{- s.heading -}}
          </{{ heading_tag }}>
        {%- endif -%}

        {%- if s.text != blank -%}
          <div class="lib-video-hero__text">{{ s.text }}</div>
        {%- endif -%}

        {%- if section.blocks.size > 0 -%}
          <div class="lib-video-hero__actions">
            {%- for block in section.blocks -%}
              {%- liquid
                assign arrow = true
                if forloop.index > 1
                  assign arrow = false
                endif
              -%}
              <span class="lib-video-hero__action" {{ block.shopify_attributes }}>
                {%- render 'lib-button',
                  label: block.settings.label,
                  url: block.settings.url,
                  style: block.settings.style,
                  new_tab: block.settings.new_tab,
                  arrow: arrow
                -%}
              </span>
            {%- endfor -%}
          </div>
        {%- endif -%}
      </div>
    </div>

    {%- if has_video -%}
      <button
        type="button"
        class="lib-video-hero__toggle"
        data-state="paused"
        data-pause-label="{{ s.pause_label | escape }}"
        data-play-label="{{ s.play_label | escape }}"
        aria-label="{{ s.play_label | escape }}"
        hidden
      >
        <svg class="lib-video-hero__icon lib-video-hero__icon--pause" viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false">
          <path d="M6.5 4.5v11M13.5 4.5v11" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>
        </svg>
        <svg class="lib-video-hero__icon lib-video-hero__icon--play" viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false">
          <path d="M6.5 4.2v11.6L16 10z" fill="currentColor"/>
        </svg>
      </button>
    {%- endif -%}
  </div>
</lib-video-hero>

{% stylesheet %}
  .lib-video-hero {
    display: block;
    background-color: var(--lib-bg);
    color: var(--lib-text);
    padding: var(--lib-pt-m) 0 var(--lib-pb-m);
  }
  .lib-video-hero--page { padding-inline: 1rem; }
  .lib-video-hero__frame {
    position: relative;
    display: grid;
    min-height: 480px;
    isolation: isolate;
    overflow: hidden;
    background: color-mix(in srgb, var(--lib-text) 12%, #1c1917);
  }
  .lib-video-hero--page .lib-video-hero__frame {
    max-width: 1200px;
    margin-inline: auto;
    border-radius: var(--lib-radius-frame);
  }

  /* Height: mobile first, then desktop. */
  .lib-video-hero[data-height-m='small'] .lib-video-hero__frame { min-height: 360px; }
  .lib-video-hero[data-height-m='medium'] .lib-video-hero__frame { min-height: 480px; }
  .lib-video-hero[data-height-m='large'] .lib-video-hero__frame { min-height: 600px; }
  .lib-video-hero[data-height-m='full'] .lib-video-hero__frame { min-height: 100vh; min-height: 100svh; }
  @media (min-width: 750px) {
    .lib-video-hero { padding-top: var(--lib-pt); padding-bottom: var(--lib-pb); }
    .lib-video-hero[data-height='small'] .lib-video-hero__frame { min-height: 440px; }
    .lib-video-hero[data-height='medium'] .lib-video-hero__frame { min-height: 600px; }
    .lib-video-hero[data-height='large'] .lib-video-hero__frame { min-height: 760px; }
    .lib-video-hero[data-height='full'] .lib-video-hero__frame { min-height: 100vh; min-height: 100svh; }
  }

  /* Visibility */
  @media (max-width: 749px) {
    .lib-video-hero[data-show='desktop'] { display: none; }
    .lib-video-hero__video[data-device='desktop'] { display: none; }
  }
  @media (min-width: 750px) {
    .lib-video-hero[data-show='mobile'] { display: none; }
    .lib-video-hero__video[data-device='mobile'] { display: none; }
  }

  /* Poster, video and overlay fill the frame; the video fades in over the poster once it plays. */
  .lib-video-hero__media,
  .lib-video-hero__overlay {
    position: absolute;
    inset: 0;
  }
  .lib-video-hero__media { z-index: -2; overflow: hidden; border-radius: inherit; }
  .lib-video-hero__img,
  .lib-video-hero__video {
    position: absolute;
    inset: 0;
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
  .lib-video-hero__img:is(svg) { fill: color-mix(in srgb, #fff 18%, transparent); background: #57534e; }
  .lib-video-hero__video { opacity: 0; transition: opacity 0.6s ease; }
  .lib-video-hero__video.is-ready { opacity: 1; }
  .lib-video-hero__overlay {
    z-index: -1;
    border-radius: inherit;
    background: var(--lib-overlay);
    opacity: var(--lib-overlay-o);
    pointer-events: none;
  }

  /* Content position: mobile uses data-pos-m (top / middle / bottom), desktop the 9-way data-pos. */
  .lib-video-hero__content {
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    min-width: 0;
    padding: 2rem 1.25rem 5rem;
  }
  .lib-video-hero__content[data-pos-m='top'] { justify-content: flex-start; }
  .lib-video-hero__content[data-pos-m='middle'] { justify-content: center; }
  .lib-video-hero__content[data-pos-m='bottom'] { justify-content: flex-end; }
  .lib-video-hero__content[data-align-m='left'] { align-items: flex-start; }
  .lib-video-hero__content[data-align-m='center'] { align-items: center; }
  .lib-video-hero__content[data-align-m='right'] { align-items: flex-end; }
  @media (min-width: 750px) {
    .lib-video-hero__content { padding: clamp(2.5rem, 5vw, 4.5rem) clamp(2rem, 5vw, 4.5rem); }
    .lib-video-hero--full .lib-video-hero__content {
      padding-inline: max(clamp(2rem, 5vw, 4.5rem), calc((100% - 1200px) / 2 + 1rem));
    }
    .lib-video-hero__content[data-pos^='top'] { justify-content: flex-start; }
    .lib-video-hero__content[data-pos^='middle'] { justify-content: center; }
    .lib-video-hero__content[data-pos^='bottom'] { justify-content: flex-end; }
    .lib-video-hero__content[data-pos$='left'] { align-items: flex-start; }
    .lib-video-hero__content[data-pos$='center'] { align-items: center; }
    .lib-video-hero__content[data-pos$='right'] { align-items: flex-end; }
  }

  /* Content box: transparent, or a solid card with its own colors. */
  .lib-video-hero__box {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    width: 100%;
    max-width: var(--lib-content-w);
    text-align: left;
  }
  .lib-video-hero__box[data-box='solid'] {
    --lib-text: var(--lib-box-text);
    --lib-accent: var(--lib-box-text);
    --lib-accent-text: var(--lib-box);
    color: var(--lib-box-text);
    background: var(--lib-box);
    padding: clamp(1.5rem, 4vw, 2.75rem);
    border-radius: 4px;
    box-shadow: 0 24px 48px -28px rgb(0 0 0 / 0.45);
  }
  .lib-video-hero__box[data-align-m='center'] { align-items: center; text-align: center; }
  .lib-video-hero__box[data-align-m='right'] { align-items: flex-end; text-align: right; }
  @media (min-width: 750px) {
    .lib-video-hero__box { width: auto; }
    .lib-video-hero__box[data-align='left'] { align-items: flex-start; text-align: left; }
    .lib-video-hero__box[data-align='center'] { align-items: center; text-align: center; }
    .lib-video-hero__box[data-align='right'] { align-items: flex-end; text-align: right; }
  }

  .lib-video-hero__eyebrow {
    margin: 0 0 1rem;
    font-size: 0.8rem;
    font-weight: 600;
    letter-spacing: 0.16em;
    text-transform: uppercase;
  }
  .lib-video-hero__heading {
    margin: 0;
    font-size: clamp(2.25rem, 5vw, 4rem);
    line-height: 1.05;
    letter-spacing: -0.025em;
    text-wrap: balance;
    overflow-wrap: anywhere;
  }
  .lib-video-hero__heading--small { font-size: clamp(1.75rem, 3vw, 2.5rem); }
  .lib-video-hero__heading--medium { font-size: clamp(2rem, 4vw, 3.25rem); }
  .lib-video-hero__heading--xlarge { font-size: clamp(2.75rem, 7vw, 5.5rem); }
  .lib-video-hero__text {
    margin-top: 1.125rem;
    font-size: 1.0625rem;
    line-height: 1.6;
    opacity: 0.9;
  }
  .lib-video-hero__text > :first-child { margin-top: 0; }
  .lib-video-hero__text > :last-child { margin-bottom: 0; }
  .lib-video-hero__text a { color: inherit; }
  .lib-video-hero__action { display: contents; }
  .lib-video-hero__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    margin-top: 1.75rem;
  }
  .lib-video-hero__box[data-align-m='center'] .lib-video-hero__actions { justify-content: center; }
  .lib-video-hero__box[data-align-m='right'] .lib-video-hero__actions { justify-content: flex-end; }
  @media (min-width: 750px) {
    .lib-video-hero__box[data-align='left'] .lib-video-hero__actions { justify-content: flex-start; }
    .lib-video-hero__box[data-align='center'] .lib-video-hero__actions { justify-content: center; }
    .lib-video-hero__box[data-align='right'] .lib-video-hero__actions { justify-content: flex-end; }
  }

  /* Pause / play: a 44px round button in the bottom corner, always above the overlay. */
  .lib-video-hero__toggle {
    position: absolute;
    right: 1rem;
    bottom: 1rem;
    z-index: 2;
    display: grid;
    place-items: center;
    width: 2.75rem;
    height: 2.75rem;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: rgb(20 18 16 / 0.6);
    color: #fff;
    cursor: pointer;
    transition: background-color 0.2s ease;
  }
  .lib-video-hero__toggle[hidden] { display: none; }
  .lib-video-hero__toggle:hover { background: rgb(20 18 16 / 0.8); }
  .lib-video-hero__toggle:focus-visible { outline: 2px solid #fff; outline-offset: 3px; box-shadow: 0 0 0 5px rgb(20 18 16 / 0.7); }
  .lib-video-hero__toggle[data-state='playing'] .lib-video-hero__icon--play,
  .lib-video-hero__toggle[data-state='paused'] .lib-video-hero__icon--pause { display: none; }
  @media (min-width: 750px) {
    .lib-video-hero__toggle { right: 1.5rem; bottom: 1.5rem; }
  }

  /* Motion: the copy rises once on load. */
  .lib-video-hero[data-animate] .lib-video-hero__box > * { animation: lib-video-hero-rise 0.8s cubic-bezier(0.2, 0.7, 0.2, 1) both; }
  .lib-video-hero[data-animate] .lib-video-hero__box > :nth-child(2) { animation-delay: 0.08s; }
  .lib-video-hero[data-animate] .lib-video-hero__box > :nth-child(3) { animation-delay: 0.16s; }
  .lib-video-hero[data-animate] .lib-video-hero__box > :nth-child(n + 4) { animation-delay: 0.24s; }
  @keyframes lib-video-hero-rise {
    from { opacity: 0; transform: translateY(14px); }
  }
  @media (prefers-reduced-motion: reduce) {
    .lib-video-hero[data-animate] .lib-video-hero__box > * { animation: none; }
    .lib-video-hero__video { transition: none; }
  }
{% endstylesheet %}

{% javascript %}
  if (!customElements.get('lib-video-hero')) {
    customElements.define(
      'lib-video-hero',
      class extends HTMLElement {
        connectedCallback() {
          this.videos = [...this.querySelectorAll('video[data-media]')];
          this.button = this.querySelector('.lib-video-hero__toggle');
          if (!this.videos.length || !this.button) return;
          this.queries = this.videos.map((video) => window.matchMedia(video.dataset.media));
          // Visitors who ask for reduced motion start paused; they can still press play.
          this.userPaused = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
          this.visible = false;
          this.cleanup = [];

          const listen = (target, type, fn) => {
            target.addEventListener(type, fn);
            this.cleanup.push(() => target.removeEventListener(type, fn));
          };
          const update = () => this.update();
          listen(this.button, 'click', () => {
            this.userPaused = !this.userPaused;
            this.update();
          });
          this.queries.forEach((query) => listen(query, 'change', update));
          listen(document, 'visibilitychange', update);
          this.videos.forEach((video) => listen(video, 'playing', () => video.classList.add('is-ready')));

          this.observer = new IntersectionObserver(
            ([entry]) => {
              this.visible = entry.isIntersecting;
              this.update();
            },
            {rootMargin: '200px 0px'},
          );
          this.observer.observe(this);
          this.button.hidden = false;
          this.update();
        }

        disconnectedCallback() {
          this.observer?.disconnect();
          (this.cleanup || []).forEach((fn) => fn());
          this.cleanup = [];
        }

        update() {
          const active = this.videos.find((video, i) => this.queries[i].matches);
          this.videos.forEach((video) => {
            if (video !== active) video.pause();
          });
          const shouldPlay = Boolean(active) && this.visible && !this.userPaused && !document.hidden;
          if (active && shouldPlay) {
            // The file is only requested the first time this video should play.
            const source = active.querySelector('source[data-src]');
            if (source) {
              source.src = source.dataset.src;
              source.removeAttribute('data-src');
              active.load();
            }
            const playing = active.play();
            if (playing) {
              playing.catch(() => {
                // Autoplay refused (data saver, browser policy): show the play button instead.
                this.userPaused = true;
                this.setButton(false);
              });
            }
          } else if (active) {
            active.pause();
          }
          this.button.hidden = !active;
          this.setButton(!this.userPaused);
        }

        setButton(playing) {
          this.button.dataset.state = playing ? 'playing' : 'paused';
          this.button.setAttribute('aria-label', playing ? this.button.dataset.pauseLabel : this.button.dataset.playLabel);
        }
      },
    );
  }
{% endjavascript %}

{% schema %}
{
  "name": "Lib · Video hero",
  "tag": "section",
  "class": "lib-section",
  "settings": [
    { "type": "header", "content": "Video" },
    { "type": "video", "id": "video", "label": "Desktop video", "info": "Upload to Shopify. Plays muted and looped; keep it short (10–30 s) and without important text." },
    { "type": "video", "id": "video_mobile", "label": "Mobile video", "info": "Optional. Plays below 750px; a portrait video works best. Without it the desktop video plays everywhere." },
    { "type": "image_picker", "id": "image", "label": "Desktop poster image", "info": "Shows while the video loads, and instead of it when motion is reduced or JavaScript is off." },
    { "type": "image_picker", "id": "image_mobile", "label": "Mobile poster image", "info": "Optional. Shown below 750px." },
    { "type": "text", "id": "pause_label", "label": "Pause button label", "info": "Read by screen readers.", "default": "Pause background video" },
    { "type": "text", "id": "play_label", "label": "Play button label", "info": "Read by screen readers.", "default": "Play background video" },
    { "type": "color", "id": "overlay_color", "label": "Overlay color", "default": "#1C1917" },
    { "type": "range", "id": "overlay_opacity", "label": "Overlay opacity", "min": 0, "max": 90, "step": 5, "unit": "%", "default": 35 },

    { "type": "header", "content": "Content" },
    { "type": "text", "id": "eyebrow", "label": "Eyebrow", "default": "Made in our studio" },
    { "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "Shaped by hand, <em>fired with care</em>" },
    {
      "type": "select",
      "id": "heading_tag",
      "label": "Heading tag",
      "info": "Use H1 only when this is the page's main heading.",
      "options": [
        { "value": "h1", "label": "H1" },
        { "value": "h2", "label": "H2" },
        { "value": "h3", "label": "H3" },
        { "value": "p", "label": "Paragraph" }
      ],
      "default": "h2"
    },
    {
      "type": "select",
      "id": "heading_size",
      "label": "Heading size",
      "options": [
        { "value": "small", "label": "Small" },
        { "value": "medium", "label": "Medium" },
        { "value": "large", "label": "Large" },
        { "value": "xlarge", "label": "Extra large" }
      ],
      "default": "large"
    },
    { "type": "richtext", "id": "text", "label": "Text", "default": "<p>Small-batch stoneware for everyday tables, glazed in earthy tones and made to last.</p>" },

    { "type": "header", "content": "Layout" },
    {
      "type": "select",
      "id": "height",
      "label": "Height on desktop",
      "options": [
        { "value": "small", "label": "Small" },
        { "value": "medium", "label": "Medium" },
        { "value": "large", "label": "Large" },
        { "value": "full", "label": "Full screen" }
      ],
      "default": "large"
    },
    {
      "type": "select",
      "id": "height_mobile",
      "label": "Height on mobile",
      "options": [
        { "value": "small", "label": "Small" },
        { "value": "medium", "label": "Medium" },
        { "value": "large", "label": "Large" },
        { "value": "full", "label": "Full screen" }
      ],
      "default": "large"
    },
    {
      "type": "select",
      "id": "section_width",
      "label": "Width",
      "options": [
        { "value": "page", "label": "Page width" },
        { "value": "full", "label": "Full width" }
      ],
      "default": "full"
    },
    {
      "type": "range",
      "id": "corner_radius",
      "label": "Corner radius",
      "min": 0,
      "max": 40,
      "step": 2,
      "unit": "px",
      "default": 12,
      "visible_if": "{{ section.settings.section_width == 'page' }}"
    },
    {
      "type": "select",
      "id": "content_position",
      "label": "Content position on desktop",
      "info": "Text aligns to the same side: left, center or right.",
      "options": [
        { "value": "top-left", "label": "Top left" },
        { "value": "top-center", "label": "Top center" },
        { "value": "top-right", "label": "Top right" },
        { "value": "middle-left", "label": "Middle left" },
        { "value": "middle-center", "label": "Middle center" },
        { "value": "middle-right", "label": "Middle right" },
        { "value": "bottom-left", "label": "Bottom left" },
        { "value": "bottom-center", "label": "Bottom center" },
        { "value": "bottom-right", "label": "Bottom right" }
      ],
      "default": "middle-left"
    },
    {
      "type": "select",
      "id": "content_position_mobile",
      "label": "Content position on mobile",
      "options": [
        { "value": "top", "label": "Top" },
        { "value": "middle", "label": "Middle" },
        { "value": "bottom", "label": "Bottom" }
      ],
      "default": "bottom"
    },
    {
      "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": "range", "id": "content_width", "label": "Content max width", "min": 320, "max": 960, "step": 20, "unit": "px", "default": 620 },
    {
      "type": "select",
      "id": "content_box",
      "label": "Content box",
      "options": [
        { "value": "none", "label": "None (text on video)" },
        { "value": "solid", "label": "Solid card" }
      ],
      "default": "none"
    },

    { "type": "header", "content": "Colors" },
    { "type": "color", "id": "color_bg", "label": "Background", "info": "Shows around the hero when it has padding or page width.", "default": "#F5EFE6" },
    { "type": "color", "id": "color_text", "label": "Text", "default": "#FFFFFF" },
    { "type": "color", "id": "color_accent", "label": "Solid button", "default": "#FFFFFF" },
    { "type": "color", "id": "color_accent_text", "label": "Text on solid button", "default": "#1C1917" },
    {
      "type": "color",
      "id": "color_box",
      "label": "Content box",
      "default": "#F5EFE6",
      "visible_if": "{{ section.settings.content_box == 'solid' }}"
    },
    {
      "type": "color",
      "id": "color_box_text",
      "label": "Text in content box",
      "info": "Also used for the solid button inside the box.",
      "default": "#2A211B",
      "visible_if": "{{ section.settings.content_box == 'solid' }}"
    },

    { "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": 0 },
    { "type": "range", "id": "padding_bottom", "label": "Bottom padding on desktop", "min": 0, "max": 200, "step": 4, "unit": "px", "default": 0 },
    { "type": "range", "id": "padding_top_mobile", "label": "Top padding on mobile", "min": 0, "max": 160, "step": 4, "unit": "px", "default": 0 },
    { "type": "range", "id": "padding_bottom_mobile", "label": "Bottom padding on mobile", "min": 0, "max": 160, "step": 4, "unit": "px", "default": 0 },
    {
      "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": "Animate text on load", "info": "Never when reduced motion is on.", "default": true }
  ],
  "blocks": [
    {
      "type": "button",
      "name": "Button",
      "limit": 2,
      "settings": [
        { "type": "text", "id": "label", "label": "Label", "default": "Shop new pieces" },
        { "type": "url", "id": "url", "label": "Link" },
        {
          "type": "select",
          "id": "style",
          "label": "Style",
          "options": [
            { "value": "primary", "label": "Solid" },
            { "value": "outline", "label": "Outline" },
            { "value": "text", "label": "Text link" }
          ],
          "default": "primary"
        },
        { "type": "checkbox", "id": "new_tab", "label": "Open link in a new tab", "default": false }
      ]
    }
  ],
  "presets": [
    {
      "name": "Lib · Video hero",
      "blocks": [
        { "type": "button", "settings": { "label": "Shop new pieces", "style": "primary" } },
        { "type": "button", "settings": { "label": "Our story", "style": "outline" } }
      ]
    }
  ]
}
{% 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.
