Shopify Tutorial · Dawn Theme · No App

TikTok-Style Shopify Shoppable Video Feed Section in Shopify — No App Required

Your customers are used to scrolling TikTok. They stop, they watch, they tap. And right now, your Shopify store is making them read a product description instead. In this tutorial, I'll show you how to add a scrollable TikTok-style video feed section to your Shopify storefront — completely free, no app required. We'll use Liquid, CSS, and Swiper.js. Built and tested on the Dawn theme, but it works on any theme that supports custom sections. Each video card plays on tap, supports mute/unmute, and auto-plays as it scrolls into view. A product tile sits below each video so customers can go straight to the product page.
Video Tutorial
Step-by-step guide
01
Create the section file

Go to Online Store → Themes → Edit Code → Sections and create a new file. Name it exactly:

video-feed.liquid

Then paste the full section code below and save:

Liquid
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css" />

{%- style -%}
  .section-{{ section.id }}-padding {
    padding-top: {{ section.settings.padding_top | times: 0.75 | round: 0 }}px;
    padding-bottom: {{ section.settings.padding_bottom | times: 0.75 | round: 0 }}px;
  }
  @media screen and (min-width: 750px) {
    .section-{{ section.id }}-padding {
      padding-top: {{ section.settings.padding_top }}px;
      padding-bottom: {{ section.settings.padding_bottom }}px;
    }
  }
  .custom-video-feed { position: relative; }
  .custom-video-feed__header {
    display: flex; align-items: center;
    justify-content: flex-end; margin-bottom: 24px;
  }
  .custom-video-feed__nav { display: flex; gap: 8px; }
  .custom-video-feed__nav-btn {
    width: 40px; height: 40px; border-radius: 50%;
    border: 1.5px solid rgba(0,0,0,0.35); background: transparent;
    cursor: pointer; display: flex; align-items: center;
    justify-content: center; transition: all 0.25s ease;
    color: #282928; flex-shrink: 0;
  }
  .custom-video-feed__nav-btn:hover:not(.swiper-button-disabled) {
    background: #121212; color: #fff; border-color: #121212;
  }
  .custom-video-feed__nav-btn.swiper-button-disabled { opacity: 0.3; cursor: not-allowed; }
  .custom-video-feed__nav-btn svg { width: 16px; height: 16px; }
  .custom-video-feed__nav-btn--prev svg { transform: rotate(180deg); }
  .custom-video-feed .swiper-pagination {
    position: static; margin-top: 16px;
    display: flex; justify-content: center; gap: 6px;
  }
  .custom-video-feed .swiper-pagination-bullet {
    width: 6px; height: 6px; border-radius: 50%;
    background: rgba(0,0,0,0.2); opacity: 1; margin: 0 !important;
    transition: all 0.3s ease;
  }
  .custom-video-feed .swiper-pagination-bullet-active {
    background: #121212; width: 20px; border-radius: 3px;
  }
  .custom-video-feed__card {
    display: flex; flex-direction: column;
    overflow: hidden; cursor: pointer;
  }
  .custom-video-feed__video-wrap {
    position: relative; width: 100%; aspect-ratio: 9 / 16;
    overflow: hidden; background: #000; border-radius: 10px;
  }
  .custom-video-feed__video {
    width: 100%; height: 100%; object-fit: cover;
    display: block; cursor: pointer;
  }
  .custom-video-feed__controls {
    position: absolute; bottom: 10px; right: 10px; z-index: 2;
  }
  .custom-video-feed__ctrl-btn {
    width: 35px; height: 35px; border-radius: 10px; border: none;
    background: rgba(255,255,255,0.2); color: white;
    display: flex; align-items: center; justify-content: center;
    cursor: pointer; backdrop-filter: blur(4px);
    transition: background 0.2s ease;
  }
  .custom-video-feed__ctrl-btn:hover { background: rgba(255,255,255,0.3); }
  .custom-video-feed__ctrl-btn svg { width: 18px; height: 18px; }
  .custom-video-feed__play-overlay {
    position: absolute; inset: 0; display: flex;
    align-items: center; justify-content: center;
    background: rgba(0,0,0,0.2); opacity: 1;
    transition: opacity 0.3s ease; pointer-events: none;
  }
  .custom-video-feed__play-overlay.is-playing { opacity: 0; }
  .custom-video-feed__play-icon {
    width: 48px; height: 48px; border-radius: 50%;
    background: rgba(255,255,255,0.9);
    display: flex; align-items: center; justify-content: center;
  }
  .custom-video-feed__play-icon svg { width: 18px; height: 18px; margin-left: 3px; color: #000; }
  .custom-video-feed__product {
    display: flex; align-items: center; gap: 10px;
    padding: 10px 12px; border: 1px solid #e1e1e1;
    text-decoration: none; color: inherit;
    transition: background 0.2s ease; border-radius: 10px;
    margin-top: 4px; width: 100%; box-sizing: border-box;
    margin-bottom: 24px;
  }
  .custom-video-feed__product:hover { background: rgba(0,0,0,0.03); }
  .custom-video-feed__product-img {
    width: 55px; height: 55px; object-fit: cover;
    border-radius: 10px; flex-shrink: 0;
    border: 1px solid rgba(0,0,0,0.08);
  }
  .custom-video-feed__product-img-placeholder {
    width: 55px; height: 55px; background: rgba(0,0,0,0.06);
    border-radius: 10px; flex-shrink: 0;
  }
  .custom-video-feed__product-info { flex: 1; min-width: 0; text-align: left; }
  .custom-video-feed__product-title {
    font-size: 16px; font-weight: 400; margin: 0 0 8px;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    line-height: 1.2; color: #282928;
  }
  .custom-video-feed__product-price {
    font-size: 18px; font-weight: 600; line-height: 1.2; color: #282928; margin: 0;
  }
  .custom-video-feed__product-btn {
    width: 32px; height: 32px; border-radius: 10px;
    background: #000; color: #fff; border: none;
    display: flex; align-items: center; justify-content: center;
    cursor: pointer; flex-shrink: 0; transition: opacity 0.2s ease;
  }
  .custom-video-feed__product-btn:hover { opacity: 0.8; }
  .custom-video-feed__product-btn svg { width: 14px; height: 14px; }
{%- endstyle -%}

<div class="page-width section-{{ section.id }}-padding">

  <div class="custom-video-feed__header">
    <div class="custom-video-feed__nav">
      <button class="custom-video-feed__nav-btn custom-video-feed__nav-btn--prev"
        id="VideoFeedPrev-{{ section.id }}" aria-label="Previous">
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
          stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
          <path d="M9 18l6-6-6-6"/>
        </svg>
      </button>
      <button class="custom-video-feed__nav-btn custom-video-feed__nav-btn--next"
        id="VideoFeedNext-{{ section.id }}" aria-label="Next">
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
          stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
          <path d="M9 18l6-6-6-6"/>
        </svg>
      </button>
    </div>
  </div>

  <div class="custom-video-feed">
    <div class="swiper" id="VideoFeedSwiper-{{ section.id }}">
      <div class="swiper-wrapper">

        {%- for block in section.blocks -%}
          {%- if block.type == 'video_card' -%}
            {%- assign linked_product = all_products[block.settings.product] -%}
            <div class="swiper-slide">
              <div class="custom-video-feed__card">

                <div class="custom-video-feed__video-wrap">
                  {%- if block.settings.video_url != blank -%}
                    <video class="custom-video-feed__video"
                      src="{{ block.settings.video_url }}"
                      loop muted playsinline preload="metadata"></video>
                  {%- else -%}
                    <div style="width:100%;height:100%;background:rgba(0,0,0,0.06);
                      display:flex;align-items:center;justify-content:center;">
                      <svg width="40" height="40" viewBox="0 0 24 24" fill="none"
                        stroke="rgba(0,0,0,0.2)" stroke-width="1.5">
                        <polygon points="5 3 19 12 5 21 5 3"/>
                      </svg>
                    </div>
                  {%- endif -%}

                  <div class="custom-video-feed__play-overlay">
                    <div class="custom-video-feed__play-icon">
                      <svg viewBox="0 0 24 24" fill="currentColor">
                        <polygon points="5 3 19 12 5 21 5 3"/>
                      </svg>
                    </div>
                  </div>

                  <div class="custom-video-feed__controls">
                    <button class="custom-video-feed__ctrl-btn" aria-label="Toggle mute" data-muted="true">
                      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                        <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
                        <line x1="23" y1="9" x2="17" y2="15"/>
                        <line x1="17" y1="9" x2="23" y2="15"/>
                      </svg>
                    </button>
                  </div>
                </div>

                {%- if block.settings.product != blank and linked_product != blank -%}
                  <a href="{{ linked_product.url }}" class="custom-video-feed__product">
                    {%- if linked_product.featured_image -%}
                      <img class="custom-video-feed__product-img"
                        src="{{ linked_product.featured_image | image_url: width: 120 }}"
                        alt="{{ linked_product.featured_image.alt | escape }}"
                        width="55" height="55" loading="lazy">
                    {%- else -%}
                      <div class="custom-video-feed__product-img-placeholder"></div>
                    {%- endif -%}
                    <div class="custom-video-feed__product-info">
                      <p class="custom-video-feed__product-title">{{ linked_product.title }}</p>
                      <p class="custom-video-feed__product-price">{{ linked_product.price | money }}</p>
                    </div>
                    <span class="custom-video-feed__product-btn" aria-hidden="true">
                      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
                        stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
                        <path d="M5 12h14M12 5l7 7-7 7"/>
                      </svg>
                    </span>
                  </a>
                {%- endif -%}

              </div>
            </div>
          {%- endif -%}
        {%- endfor -%}

      </div>
      <div class="swiper-pagination" id="VideoFeedPagination-{{ section.id }}"></div>
    </div>
  </div>

</div>

<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js" defer></script>

<script>
document.addEventListener('DOMContentLoaded', function () {
  const sectionId = '{{ section.id }}';

  const swiper = new Swiper('#VideoFeedSwiper-' + sectionId, {
    grabCursor: true,
    navigation: {
      prevEl: '#VideoFeedPrev-' + sectionId,
      nextEl: '#VideoFeedNext-' + sectionId,
    },
    pagination: {
      el: '#VideoFeedPagination-' + sectionId,
      clickable: true,
    },
    breakpoints: {
      0:    { slidesPerView: 2, spaceBetween: 10 },
      600:  { slidesPerView: 2, spaceBetween: 12 },
      768:  { slidesPerView: 3, spaceBetween: 12 },
      1024: { slidesPerView: 5, spaceBetween: 12 },
    },
  });

  const allVideos = document.querySelectorAll(
    '#VideoFeedSwiper-' + sectionId + ' .custom-video-feed__video'
  );

  function pauseAll(except) {
    allVideos.forEach(function (v) {
      if (v !== except) {
        v.pause();
        const overlay = v.closest('.custom-video-feed__video-wrap')
          .querySelector('.custom-video-feed__play-overlay');
        if (overlay) overlay.classList.remove('is-playing');
      }
    });
  }

  allVideos.forEach(function (video) {
    const wrap    = video.closest('.custom-video-feed__video-wrap');
    const overlay = wrap.querySelector('.custom-video-feed__play-overlay');
    const muteBtn = wrap.querySelector('.custom-video-feed__ctrl-btn');

    wrap.addEventListener('click', function (e) {
      if (muteBtn && (e.target === muteBtn || muteBtn.contains(e.target))) return;
      if (video.paused) {
        pauseAll(video);
        video.play().catch(function () {});
        if (overlay) overlay.classList.add('is-playing');
      } else {
        video.pause();
        if (overlay) overlay.classList.remove('is-playing');
      }
    });

    if (muteBtn) {
      muteBtn.addEventListener('click', function (e) {
        e.stopPropagation();
        video.muted = !video.muted;
        muteBtn.setAttribute('data-muted', video.muted ? 'true' : 'false');
        muteBtn.innerHTML = video.muted
          ? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/></svg>'
          : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/></svg>';
      });
    }
  });

  if ('IntersectionObserver' in window) {
    const observer = new IntersectionObserver(function (entries) {
      entries.forEach(function (entry) {
        const video   = entry.target.querySelector('.custom-video-feed__video');
        const overlay = entry.target.querySelector('.custom-video-feed__play-overlay');
        if (!video) return;
        if (entry.isIntersecting) {
          video.play().catch(function () {});
          if (overlay) overlay.classList.add('is-playing');
        } else {
          video.pause();
          if (overlay) overlay.classList.remove('is-playing');
        }
      });
    }, { threshold: 0.5 });

    document.querySelectorAll(
      '#VideoFeedSwiper-' + sectionId + ' .swiper-slide'
    ).forEach(function (slide) { observer.observe(slide); });
  }
});
</script>

{% schema %}
{
  "name": "Video Feed",
  "tag": "section",
  "class": "section",
  "disabled_on": { "groups": ["header", "footer"] },
  "settings": [
    { "type": "header", "content": "Padding" },
    { "type": "range", "id": "padding_top", "min": 0, "max": 100,
      "step": 4, "unit": "px", "label": "Padding top", "default": 36 },
    { "type": "range", "id": "padding_bottom", "min": 0, "max": 100,
      "step": 4, "unit": "px", "label": "Padding bottom", "default": 36 }
  ],
  "blocks": [
    {
      "type": "video_card",
      "name": "Video Card",
      "settings": [
        { "type": "header", "content": "Video" },
        { "type": "url", "id": "video_url", "label": "Paste video URL (.mp4)",
          "info": "Use if video is hosted externally" },
        { "type": "header", "content": "Product" },
        { "type": "product", "id": "product", "label": "Select product" }
      ]
    }
  ],
  "presets": [{ "name": "Video Feed", "blocks": [] }]
}
{% endschema %}
02
Add the section to your theme

Go to Online Store → Themes → Customize. On the Homepage (or any page), click Add section and search for Video Feed. Add it and drag it into position.

💡
The section is fully standalone — it doesn't touch any existing theme files. Safe to add on any page.
03
Add Video Card blocks

Inside the Video Feed section in the theme editor, click Add block → Video Card. Each block has two fields:

· Video URL (.mp4) — paste a direct link to your hosted video
· Select product — choose any product from your store

Add as many blocks as you need. Each block = one video card in the slider.

04
Save and preview

Click Save in the theme editor, then open your storefront. Your video feed appears with the linked product tiles below each card.

Videos auto-play as they enter the viewport and pause when they scroll out. Only one video plays at a time — clicking a new card pauses the previous one automatically.

📐
To adjust how many slides show at each breakpoint, change the slidesPerView values inside the breakpoints object in the script. The defaults are 2 on mobile, 3 on tablet, 5 on desktop.
🎉

Save all files and preview

Open your storefront, add items to the feed, and watch it come to life. Tap any card to play, use the mute button for audio, and the nav arrows and pagination dots work automatically. Adjust slidesPerView in the Swiper config to fit your theme's column width.

Available for new projects

Want this built for your store?

I implement this kind of custom Shopify development daily — no apps, clean code, done fast. Tell me what you need and I'll get it done right.

60+ custom stores built
5★ Upwork rating
Reply within 24hrs
Book a Free Call Send a message
NO COMMITMENT · REPLIES WITHIN 24HRS