10 Custom Shopify Liquid Section Examples

Custom Shopify Liquid sections give you full control over your store’s layout, design, and functionality without installing apps or writing client-side JavaScript. Every section in this guide includes the complete Liquid template and schema JSON so you can copy-paste it directly into your theme’s sections/ directory and start customizing from the editor immediately.

Online Store 2.0 made sections available on every page, not just the homepage. But most stores still rely on the same default sections their theme shipped with. After building and optimizing sections across 100+ client stores over 12 years, I have a library of production-tested components that solve real problems.

These 10 sections cover the most common requests I get from DTC brands: social proof, product education, conversion nudges, and content flexibility. Each one is built with clean Liquid, minimal CSS, and zero external dependencies. For the underlying Liquid architecture patterns these sections use, start with my Shopify Liquid development guide.

Use this when you need rotating customer testimonials on your homepage or landing pages. Each testimonial is a block, so merchants can add, remove, and reorder them from the theme editor without touching code.

This section uses pure CSS for the carousel animation and a small JavaScript snippet for auto-rotation and manual navigation. No Swiper, no Slick, no external libraries.

{% raw %}
<style>
  .testimonial-carousel { position: relative; overflow: hidden; padding: 40px 20px; max-width: 800px; margin: 0 auto; }
  .testimonial-carousel__track { display: flex; transition: transform 0.5s ease; }
  .testimonial-carousel__slide { min-width: 100%; padding: 0 20px; box-sizing: border-box; }
  .testimonial-carousel__quote { font-size: 18px; line-height: 1.6; font-style: italic; color: #333; margin-bottom: 16px; }
  .testimonial-carousel__author { font-weight: 600; color: #111; }
  .testimonial-carousel__role { font-size: 14px; color: #666; margin-top: 4px; }
  .testimonial-carousel__stars { color: #f4b400; margin-bottom: 12px; font-size: 20px; }
  .testimonial-carousel__nav { display: flex; justify-content: center; gap: 8px; margin-top: 20px; }
  .testimonial-carousel__dot { width: 10px; height: 10px; border-radius: 50%; background: #ccc; border: none; cursor: pointer; padding: 0; }
  .testimonial-carousel__dot--active { background: #111; }
  .testimonial-carousel__heading { text-align: center; margin-bottom: 24px; font-size: 28px; }
</style>

<section class="testimonial-carousel" data-section-id="{{ section.id }}">
  {% if section.settings.heading != blank %}
    <h2 class="testimonial-carousel__heading">{{ section.settings.heading }}</h2>
  {% endif %}

  <div class="testimonial-carousel__track" id="carousel-track-{{ section.id }}">
    {% for block in section.blocks %}
      <div class="testimonial-carousel__slide" {{ block.shopify_attributes }}>
        {% if block.settings.rating > 0 %}
          <div class="testimonial-carousel__stars">
            {% for i in (1..block.settings.rating) %}&#9733;{% endfor %}
          </div>
        {% endif %}
        <blockquote class="testimonial-carousel__quote">
          "{{ block.settings.quote }}"
        </blockquote>
        <div class="testimonial-carousel__author">{{ block.settings.author }}</div>
        {% if block.settings.role != blank %}
          <div class="testimonial-carousel__role">{{ block.settings.role }}</div>
        {% endif %}
      </div>
    {% endfor %}
  </div>

  {% if section.blocks.size > 1 %}
    <div class="testimonial-carousel__nav" id="carousel-nav-{{ section.id }}">
      {% for block in section.blocks %}
        <button class="testimonial-carousel__dot {% if forloop.index0 == 0 %}testimonial-carousel__dot--active{% endif %}"
                data-index="{{ forloop.index0 }}"
                aria-label="Go to testimonial {{ forloop.index }}">
        </button>
      {% endfor %}
    </div>
  {% endif %}
</section>

<script>
  (function() {
    var sectionId = '{{ section.id }}';
    var track = document.getElementById('carousel-track-' + sectionId);
    var nav = document.getElementById('carousel-nav-' + sectionId);
    if (!track) return;
    var slides = track.children;
    var dots = nav ? nav.querySelectorAll('.testimonial-carousel__dot') : [];
    var current = 0;
    var total = slides.length;
    var autoplayMs = {{ section.settings.autoplay_speed | default: 5 }} * 1000;

    function goTo(index) {
      current = (index + total) % total;
      track.style.transform = 'translateX(-' + (current * 100) + '%)';
      dots.forEach(function(d, i) {
        d.classList.toggle('testimonial-carousel__dot--active', i === current);
      });
    }

    dots.forEach(function(dot) {
      dot.addEventListener('click', function() {
        goTo(parseInt(this.dataset.index));
      });
    });

    if (total > 1 && autoplayMs > 0) {
      setInterval(function() { goTo(current + 1); }, autoplayMs);
    }
  })();
</script>

{% schema %}
{
  "name": "Testimonial Carousel",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "What Our Customers Say"
    },
    {
      "type": "range",
      "id": "autoplay_speed",
      "label": "Autoplay speed (seconds)",
      "min": 0,
      "max": 15,
      "step": 1,
      "default": 5,
      "info": "Set to 0 to disable autoplay"
    }
  ],
  "blocks": [
    {
      "type": "testimonial",
      "name": "Testimonial",
      "settings": [
        { "type": "textarea", "id": "quote", "label": "Quote" },
        { "type": "text", "id": "author", "label": "Author name" },
        { "type": "text", "id": "role", "label": "Role or title" },
        {
          "type": "range",
          "id": "rating",
          "label": "Star rating",
          "min": 0,
          "max": 5,
          "step": 1,
          "default": 5,
          "info": "Set to 0 to hide stars"
        }
      ]
    }
  ],
  "presets": [
    {
      "name": "Testimonial Carousel",
      "blocks": [
        {
          "type": "testimonial",
          "settings": {
            "quote": "This product changed everything for us. Highly recommend.",
            "author": "Sarah M.",
            "role": "Verified Buyer",
            "rating": 5
          }
        },
        {
          "type": "testimonial",
          "settings": {
            "quote": "Fast shipping and incredible quality. Will order again.",
            "author": "James R.",
            "role": "Repeat Customer",
            "rating": 5
          }
        }
      ]
    }
  ]
}
{% endschema %}
{% endraw %}

Customization options: Adjust autoplay speed or disable it entirely from the editor. Each testimonial supports a star rating that you can set from 0 to 5 or hide completely. Add as many testimonial blocks as you need and reorder them by dragging in the editor.

2. Feature Grid with Icons

Use this on your homepage or product pages to highlight key selling points: free shipping, warranty, fast delivery, and similar trust-building features. The grid is responsive and adapts from 4 columns on desktop to 2 on tablet and 1 on mobile.

{% raw %}
<style>
  .feature-grid { padding: 40px 20px; max-width: 1200px; margin: 0 auto; }
  .feature-grid__heading { text-align: center; font-size: 28px; margin-bottom: 32px; }
  .feature-grid__items { display: grid; grid-template-columns: repeat({{ section.settings.columns }}, 1fr); gap: 24px; }
  .feature-grid__item { text-align: center; padding: 24px 16px; }
  .feature-grid__icon { font-size: 40px; margin-bottom: 12px; line-height: 1; }
  .feature-grid__icon img { width: 48px; height: 48px; object-fit: contain; }
  .feature-grid__title { font-size: 18px; font-weight: 600; margin-bottom: 8px; color: #111; }
  .feature-grid__text { font-size: 14px; line-height: 1.5; color: #555; }
  @media (max-width: 768px) {
    .feature-grid__items { grid-template-columns: repeat(2, 1fr); }
  }
  @media (max-width: 480px) {
    .feature-grid__items { grid-template-columns: 1fr; }
  }
</style>

<section class="feature-grid">
  {% if section.settings.heading != blank %}
    <h2 class="feature-grid__heading">{{ section.settings.heading }}</h2>
  {% endif %}

  <div class="feature-grid__items">
    {% for block in section.blocks %}
      <div class="feature-grid__item" {{ block.shopify_attributes }}>
        <div class="feature-grid__icon">
          {% if block.settings.icon_image != blank %}
            <img src="{{ block.settings.icon_image | image_url: width: 96 }}"
                 alt="{{ block.settings.title }}"
                 width="48" height="48"
                 loading="lazy">
          {% else %}
            {{ block.settings.icon_emoji }}
          {% endif %}
        </div>
        <h3 class="feature-grid__title">{{ block.settings.title }}</h3>
        {% if block.settings.text != blank %}
          <p class="feature-grid__text">{{ block.settings.text }}</p>
        {% endif %}
      </div>
    {% endfor %}
  </div>
</section>

{% schema %}
{
  "name": "Feature Grid",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Why Choose Us"
    },
    {
      "type": "range",
      "id": "columns",
      "label": "Columns (desktop)",
      "min": 2,
      "max": 6,
      "step": 1,
      "default": 4
    }
  ],
  "blocks": [
    {
      "type": "feature",
      "name": "Feature",
      "settings": [
        { "type": "text", "id": "icon_emoji", "label": "Icon (emoji)", "default": "\u2728" },
        { "type": "image_picker", "id": "icon_image", "label": "Icon (image, overrides emoji)" },
        { "type": "text", "id": "title", "label": "Title", "default": "Feature Title" },
        { "type": "textarea", "id": "text", "label": "Description" }
      ]
    }
  ],
  "presets": [
    {
      "name": "Feature Grid",
      "blocks": [
        { "type": "feature", "settings": { "title": "Free Shipping", "text": "On all orders over $50" } },
        { "type": "feature", "settings": { "title": "30-Day Returns", "text": "No questions asked" } },
        { "type": "feature", "settings": { "title": "Secure Checkout", "text": "SSL encrypted payments" } },
        { "type": "feature", "settings": { "title": "24/7 Support", "text": "Chat with us anytime" } }
      ]
    }
  ]
}
{% endschema %}
{% endraw %}

Customization options: Set the number of desktop columns from 2 to 6. Each feature supports either an emoji icon or an uploaded image. Merchants add, remove, and reorder features as blocks. The responsive breakpoints handle tablet and mobile automatically.

3. FAQ Accordion

Use this on product pages, landing pages, or a dedicated FAQ page. This accordion uses zero JavaScript libraries. The toggle logic is a minimal click handler that adds and removes a CSS class. Each FAQ pair is a block, so merchants manage questions from the theme editor.

This is one of the most requested sections I build for clients. On the Enea Studio project, I rebuilt their entire FAQ structure as custom sections with proper schema markup. A well-structured FAQ section feeds directly into structured data and rich results when combined with FAQPage schema.

{% raw %}
<style>
  .faq-accordion { max-width: 800px; margin: 0 auto; padding: 40px 20px; }
  .faq-accordion__heading { text-align: center; font-size: 28px; margin-bottom: 32px; }
  .faq-accordion__item { border-bottom: 1px solid #e0e0e0; }
  .faq-accordion__question { width: 100%; background: none; border: none; padding: 20px 0; font-size: 16px; font-weight: 600; text-align: left; cursor: pointer; display: flex; justify-content: space-between; align-items: center; color: #111; line-height: 1.4; }
  .faq-accordion__question:hover { color: #333; }
  .faq-accordion__icon { font-size: 20px; transition: transform 0.3s ease; flex-shrink: 0; margin-left: 16px; }
  .faq-accordion__item--open .faq-accordion__icon { transform: rotate(45deg); }
  .faq-accordion__answer { max-height: 0; overflow: hidden; transition: max-height 0.3s ease, padding 0.3s ease; }
  .faq-accordion__item--open .faq-accordion__answer { max-height: 500px; padding-bottom: 20px; }
  .faq-accordion__answer p { font-size: 15px; line-height: 1.6; color: #444; margin: 0; }
</style>

<section class="faq-accordion" data-section-id="{{ section.id }}">
  {% if section.settings.heading != blank %}
    <h2 class="faq-accordion__heading">{{ section.settings.heading }}</h2>
  {% endif %}

  {% for block in section.blocks %}
    <div class="faq-accordion__item" {{ block.shopify_attributes }}>
      <button class="faq-accordion__question" aria-expanded="false" aria-controls="faq-answer-{{ section.id }}-{{ forloop.index }}">
        {{ block.settings.question }}
        <span class="faq-accordion__icon">+</span>
      </button>
      <div class="faq-accordion__answer" id="faq-answer-{{ section.id }}-{{ forloop.index }}" role="region">
        <p>{{ block.settings.answer }}</p>
      </div>
    </div>
  {% endfor %}
</section>

<script>
  (function() {
    var section = document.querySelector('.faq-accordion[data-section-id="{{ section.id }}"]');
    if (!section) return;
    section.addEventListener('click', function(e) {
      var btn = e.target.closest('.faq-accordion__question');
      if (!btn) return;
      var item = btn.parentElement;
      var isOpen = item.classList.contains('faq-accordion__item--open');
      item.classList.toggle('faq-accordion__item--open');
      btn.setAttribute('aria-expanded', !isOpen);
    });
  })();
</script>

{% schema %}
{
  "name": "FAQ Accordion",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Frequently Asked Questions"
    }
  ],
  "blocks": [
    {
      "type": "faq",
      "name": "FAQ",
      "settings": [
        { "type": "text", "id": "question", "label": "Question" },
        { "type": "textarea", "id": "answer", "label": "Answer" }
      ]
    }
  ],
  "presets": [
    {
      "name": "FAQ Accordion",
      "blocks": [
        {
          "type": "faq",
          "settings": {
            "question": "What is your return policy?",
            "answer": "We offer a 30-day return policy on all unused items in original packaging."
          }
        },
        {
          "type": "faq",
          "settings": {
            "question": "How long does shipping take?",
            "answer": "Standard shipping takes 5-7 business days. Express shipping is available at checkout for 2-3 business day delivery."
          }
        }
      ]
    }
  ]
}
{% endschema %}
{% endraw %}

Customization options: Each FAQ pair is a block. Merchants add unlimited questions, reorder by dragging, and edit text directly in the theme editor. The accordion has proper ARIA attributes for accessibility. Adjust the max-height value in CSS if you have very long answers.

4. Before/After Image Slider

Use this for case studies, product comparisons, skincare transformations, or any before/after visual. The slider uses a draggable handle so visitors can reveal each side interactively. No external libraries required.

I built a version of this for a skincare DTC brand that needed before/after shots on every product page. It drove a measurable lift in time-on-page and product page conversion.

{% raw %}
<style>
  .ba-slider { position: relative; max-width: 700px; margin: 0 auto; padding: 40px 20px; }
  .ba-slider__heading { text-align: center; font-size: 28px; margin-bottom: 24px; }
  .ba-slider__container { position: relative; overflow: hidden; cursor: ew-resize; user-select: none; line-height: 0; }
  .ba-slider__before, .ba-slider__after { display: block; width: 100%; }
  .ba-slider__before { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; clip-path: inset(0 50% 0 0); }
  .ba-slider__after { width: 100%; height: auto; }
  .ba-slider__handle { position: absolute; top: 0; left: 50%; width: 4px; height: 100%; background: #fff; transform: translateX(-50%); z-index: 2; pointer-events: none; box-shadow: 0 0 6px rgba(0,0,0,0.3); }
  .ba-slider__handle::after { content: ''; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); width: 36px; height: 36px; background: #fff; border-radius: 50%; box-shadow: 0 2px 8px rgba(0,0,0,0.2); }
  .ba-slider__labels { display: flex; justify-content: space-between; margin-top: 8px; font-size: 13px; color: #666; }
</style>

<section class="ba-slider">
  {% if section.settings.heading != blank %}
    <h2 class="ba-slider__heading">{{ section.settings.heading }}</h2>
  {% endif %}

  {% if section.settings.before_image != blank and section.settings.after_image != blank %}
    <div class="ba-slider__container" id="ba-container-{{ section.id }}">
      <img class="ba-slider__before" id="ba-before-{{ section.id }}"
           src="{{ section.settings.before_image | image_url: width: 1400 }}"
           alt="{{ section.settings.before_label | default: 'Before' }}"
           loading="lazy">
      <img class="ba-slider__after"
           src="{{ section.settings.after_image | image_url: width: 1400 }}"
           alt="{{ section.settings.after_label | default: 'After' }}"
           loading="lazy">
      <div class="ba-slider__handle" id="ba-handle-{{ section.id }}"></div>
    </div>
    <div class="ba-slider__labels">
      <span>{{ section.settings.before_label | default: "Before" }}</span>
      <span>{{ section.settings.after_label | default: "After" }}</span>
    </div>
  {% else %}
    <p style="text-align:center;color:#999;">Upload a Before and After image in the theme editor.</p>
  {% endif %}
</section>

<script>
  (function() {
    var container = document.getElementById('ba-container-{{ section.id }}');
    if (!container) return;
    var before = document.getElementById('ba-before-{{ section.id }}');
    var handle = document.getElementById('ba-handle-{{ section.id }}');
    var dragging = false;

    function setPosition(x) {
      var rect = container.getBoundingClientRect();
      var pct = Math.max(0, Math.min(1, (x - rect.left) / rect.width));
      var clipRight = ((1 - pct) * 100).toFixed(2) + '%';
      before.style.clipPath = 'inset(0 ' + clipRight + ' 0 0)';
      handle.style.left = (pct * 100).toFixed(2) + '%';
    }

    container.addEventListener('mousedown', function() { dragging = true; });
    document.addEventListener('mouseup', function() { dragging = false; });
    container.addEventListener('mousemove', function(e) { if (dragging) setPosition(e.clientX); });
    container.addEventListener('click', function(e) { setPosition(e.clientX); });
    container.addEventListener('touchstart', function() { dragging = true; }, { passive: true });
    document.addEventListener('touchend', function() { dragging = false; });
    container.addEventListener('touchmove', function(e) {
      if (dragging) setPosition(e.touches[0].clientX);
    }, { passive: true });
  })();
</script>

{% schema %}
{
  "name": "Before/After Slider",
  "settings": [
    { "type": "text", "id": "heading", "label": "Heading", "default": "See the Difference" },
    { "type": "image_picker", "id": "before_image", "label": "Before image" },
    { "type": "image_picker", "id": "after_image", "label": "After image" },
    { "type": "text", "id": "before_label", "label": "Before label", "default": "Before" },
    { "type": "text", "id": "after_label", "label": "After label", "default": "After" }
  ],
  "presets": [
    { "name": "Before/After Slider" }
  ]
}
{% endschema %}
{% endraw %}

Customization options: Upload any two images as before and after. Labels are editable. The slider works on both desktop (mouse) and mobile (touch). Both images are lazy-loaded for performance.

5. Team/About Grid with Metafield Integration

Use this on your about page or team page to display team members in a responsive grid. Each member is a block with photo, name, role, and bio. The section also supports pulling data from metafields if you store team info there.

{% raw %}
<style>
  .team-grid { padding: 40px 20px; max-width: 1200px; margin: 0 auto; }
  .team-grid__heading { text-align: center; font-size: 28px; margin-bottom: 32px; }
  .team-grid__items { display: grid; grid-template-columns: repeat({{ section.settings.columns }}, 1fr); gap: 32px; }
  .team-grid__member { text-align: center; }
  .team-grid__photo { width: 160px; height: 160px; border-radius: 50%; object-fit: cover; margin: 0 auto 16px; display: block; }
  .team-grid__name { font-size: 18px; font-weight: 600; color: #111; margin-bottom: 4px; }
  .team-grid__role { font-size: 14px; color: #666; margin-bottom: 8px; }
  .team-grid__bio { font-size: 14px; line-height: 1.5; color: #444; }
  @media (max-width: 768px) {
    .team-grid__items { grid-template-columns: repeat(2, 1fr); }
  }
  @media (max-width: 480px) {
    .team-grid__items { grid-template-columns: 1fr; }
  }
</style>

<section class="team-grid">
  {% if section.settings.heading != blank %}
    <h2 class="team-grid__heading">{{ section.settings.heading }}</h2>
  {% endif %}

  <div class="team-grid__items">
    {% for block in section.blocks %}
      <div class="team-grid__member" {{ block.shopify_attributes }}>
        {% if block.settings.photo != blank %}
          <img class="team-grid__photo"
               src="{{ block.settings.photo | image_url: width: 320 }}"
               alt="{{ block.settings.name }}"
               width="160" height="160"
               loading="lazy">
        {% endif %}
        <div class="team-grid__name">{{ block.settings.name }}</div>
        <div class="team-grid__role">{{ block.settings.role }}</div>
        {% if block.settings.bio != blank %}
          <p class="team-grid__bio">{{ block.settings.bio }}</p>
        {% endif %}
      </div>
    {% endfor %}
  </div>
</section>

{% schema %}
{
  "name": "Team Grid",
  "settings": [
    { "type": "text", "id": "heading", "label": "Heading", "default": "Meet the Team" },
    {
      "type": "range",
      "id": "columns",
      "label": "Columns (desktop)",
      "min": 2,
      "max": 5,
      "step": 1,
      "default": 3
    }
  ],
  "blocks": [
    {
      "type": "member",
      "name": "Team Member",
      "settings": [
        { "type": "image_picker", "id": "photo", "label": "Photo" },
        { "type": "text", "id": "name", "label": "Name", "default": "Team Member" },
        { "type": "text", "id": "role", "label": "Role", "default": "Position" },
        { "type": "textarea", "id": "bio", "label": "Bio" }
      ]
    }
  ],
  "presets": [
    {
      "name": "Team Grid",
      "blocks": [
        { "type": "member", "settings": { "name": "Jane Smith", "role": "Founder & CEO" } },
        { "type": "member", "settings": { "name": "John Doe", "role": "Head of Product" } },
        { "type": "member", "settings": { "name": "Sarah Lee", "role": "Lead Designer" } }
      ]
    }
  ]
}
{% endschema %}
{% endraw %}

To pull team data from metafields instead of blocks, create a page metafield namespace like page.team_members with a JSON list, then loop over page.metafields.custom.team_members.value inside the section. This approach is useful when the same team data needs to appear across multiple pages.

Customization options: Set columns from 2 to 5. Each member supports a circular photo, name, role, and bio. Reorder by dragging blocks. Photos are lazy-loaded and served at 2x resolution for retina displays.

6. Promotional Banner with Countdown Timer

Use this for flash sales, product launches, or seasonal promotions. The countdown timer runs client-side and updates in real time. When the countdown expires, the banner either hides or displays a custom “expired” message. All settings are controlled from the theme editor.

I use a variation of this on nearly every DTC store I optimize. Urgency is one of the strongest conversion levers, but only when it is real. Set an honest deadline and let the timer do its job. For more on conversion tactics that work, see my Shopify CRO audit checklist.

{% raw %}
<style>
  .promo-banner { background: {{ section.settings.bg_color }}; color: {{ section.settings.text_color }}; padding: 24px 20px; text-align: center; }
  .promo-banner__heading { font-size: 24px; font-weight: 700; margin-bottom: 8px; }
  .promo-banner__subtext { font-size: 16px; margin-bottom: 16px; opacity: 0.9; }
  .promo-banner__countdown { display: flex; justify-content: center; gap: 16px; margin-bottom: 16px; }
  .promo-banner__unit { display: flex; flex-direction: column; align-items: center; }
  .promo-banner__number { font-size: 32px; font-weight: 700; line-height: 1; }
  .promo-banner__label { font-size: 12px; text-transform: uppercase; opacity: 0.7; margin-top: 4px; }
  .promo-banner__cta { display: inline-block; padding: 12px 32px; background: {{ section.settings.text_color }}; color: {{ section.settings.bg_color }}; text-decoration: none; font-weight: 600; font-size: 16px; border-radius: 4px; }
  .promo-banner__cta:hover { opacity: 0.9; }
  .promo-banner--expired { display: none; }
</style>

<section class="promo-banner" id="promo-{{ section.id }}" {% if section.settings.end_date == blank %}style="display:none"{% endif %}>
  {% if section.settings.heading != blank %}
    <div class="promo-banner__heading">{{ section.settings.heading }}</div>
  {% endif %}
  {% if section.settings.subtext != blank %}
    <div class="promo-banner__subtext">{{ section.settings.subtext }}</div>
  {% endif %}

  <div class="promo-banner__countdown" id="countdown-{{ section.id }}">
    <div class="promo-banner__unit"><span class="promo-banner__number" data-unit="days">00</span><span class="promo-banner__label">Days</span></div>
    <div class="promo-banner__unit"><span class="promo-banner__number" data-unit="hours">00</span><span class="promo-banner__label">Hours</span></div>
    <div class="promo-banner__unit"><span class="promo-banner__number" data-unit="mins">00</span><span class="promo-banner__label">Mins</span></div>
    <div class="promo-banner__unit"><span class="promo-banner__number" data-unit="secs">00</span><span class="promo-banner__label">Secs</span></div>
  </div>

  {% if section.settings.cta_text != blank and section.settings.cta_url != blank %}
    <a class="promo-banner__cta" href="{{ section.settings.cta_url }}">{{ section.settings.cta_text }}</a>
  {% endif %}
</section>

<script>
  (function() {
    var endDate = '{{ section.settings.end_date }}';
    if (!endDate) return;
    var end = new Date(endDate + 'T23:59:59').getTime();
    var section = document.getElementById('promo-{{ section.id }}');
    var countdown = document.getElementById('countdown-{{ section.id }}');
    if (!section || !countdown) return;

    function update() {
      var now = Date.now();
      var diff = end - now;
      if (diff <= 0) {
        section.classList.add('promo-banner--expired');
        return;
      }
      var d = Math.floor(diff / 86400000);
      var h = Math.floor((diff % 86400000) / 3600000);
      var m = Math.floor((diff % 3600000) / 60000);
      var s = Math.floor((diff % 60000) / 1000);
      countdown.querySelector('[data-unit="days"]').textContent = String(d).padStart(2, '0');
      countdown.querySelector('[data-unit="hours"]').textContent = String(h).padStart(2, '0');
      countdown.querySelector('[data-unit="mins"]').textContent = String(m).padStart(2, '0');
      countdown.querySelector('[data-unit="secs"]').textContent = String(s).padStart(2, '0');
    }
    update();
    setInterval(update, 1000);
  })();
</script>

{% schema %}
{
  "name": "Promo Banner + Countdown",
  "settings": [
    { "type": "text", "id": "heading", "label": "Heading", "default": "Flash Sale" },
    { "type": "text", "id": "subtext", "label": "Subtext", "default": "Up to 40% off everything" },
    { "type": "text", "id": "end_date", "label": "End date (YYYY-MM-DD)", "info": "Countdown will hide the banner after this date" },
    { "type": "url", "id": "cta_url", "label": "Button link" },
    { "type": "text", "id": "cta_text", "label": "Button text", "default": "Shop Now" },
    { "type": "color", "id": "bg_color", "label": "Background color", "default": "#111111" },
    { "type": "color", "id": "text_color", "label": "Text color", "default": "#ffffff" }
  ],
  "presets": [
    { "name": "Promo Banner + Countdown" }
  ]
}
{% endschema %}
{% endraw %}

Customization options: Set the end date, heading, subtext, CTA button, and colors all from the theme editor. The banner automatically hides when the countdown expires. Colors are fully configurable to match any brand.

7. Product Comparison Table

Use this to compare product variants, product bundles, or competing options side by side. Each column is a block, so merchants can add as many products as they need. This is especially effective for stores selling multiple tiers (Basic, Pro, Premium) or product bundles.

I built this for a DTC electronics brand that needed to compare three tiers of their product. The comparison table reduced support tickets asking about the differences between models by over 40%.

{% raw %}
<style>
  .comparison-table { padding: 40px 20px; max-width: 1000px; margin: 0 auto; overflow-x: auto; }
  .comparison-table__heading { text-align: center; font-size: 28px; margin-bottom: 24px; }
  .comparison-table table { width: 100%; border-collapse: collapse; min-width: 600px; }
  .comparison-table th, .comparison-table td { padding: 12px 16px; text-align: center; border-bottom: 1px solid #e0e0e0; font-size: 14px; }
  .comparison-table th { background: #f8f8f8; font-weight: 600; color: #111; }
  .comparison-table td:first-child, .comparison-table th:first-child { text-align: left; font-weight: 600; }
  .comparison-table__check { color: #22c55e; font-size: 18px; }
  .comparison-table__cross { color: #ccc; font-size: 18px; }
  .comparison-table__product-name { font-weight: 700; font-size: 16px; }
  .comparison-table__price { font-size: 14px; color: #666; margin-top: 4px; }
  .comparison-table__cta { display: inline-block; margin-top: 8px; padding: 8px 16px; background: #111; color: #fff; text-decoration: none; font-size: 13px; border-radius: 4px; }
</style>

<section class="comparison-table">
  {% if section.settings.heading != blank %}
    <h2 class="comparison-table__heading">{{ section.settings.heading }}</h2>
  {% endif %}

  {% assign features = section.settings.features | split: '|' %}

  <table>
    <thead>
      <tr>
        <th>Feature</th>
        {% for block in section.blocks %}
          <th {{ block.shopify_attributes }}>
            <div class="comparison-table__product-name">{{ block.settings.name }}</div>
            {% if block.settings.price != blank %}
              <div class="comparison-table__price">{{ block.settings.price }}</div>
            {% endif %}
            {% if block.settings.cta_url != blank %}
              <a class="comparison-table__cta" href="{{ block.settings.cta_url }}">{{ block.settings.cta_text | default: 'Buy' }}</a>
            {% endif %}
          </th>
        {% endfor %}
      </tr>
    </thead>
    <tbody>
      {% for feature in features %}
        {% assign feature_clean = feature | strip %}
        <tr>
          <td>{{ feature_clean }}</td>
          {% for block in section.blocks %}
            {% assign values = block.settings.values | split: '|' %}
            {% assign val = values[forloop.parentloop.index0] | strip | downcase %}
            <td>
              {% if val == 'yes' %}
                <span class="comparison-table__check">&#10003;</span>
              {% elsif val == 'no' %}
                <span class="comparison-table__cross">&#10005;</span>
              {% else %}
                {{ values[forloop.parentloop.index0] | strip }}
              {% endif %}
            </td>
          {% endfor %}
        </tr>
      {% endfor %}
    </tbody>
  </table>
</section>

{% schema %}
{
  "name": "Product Comparison",
  "settings": [
    { "type": "text", "id": "heading", "label": "Heading", "default": "Compare Products" },
    {
      "type": "textarea",
      "id": "features",
      "label": "Feature rows (pipe-separated)",
      "default": "Battery Life|Water Resistant|Wireless Charging|Weight",
      "info": "Separate each feature name with | (pipe character)"
    }
  ],
  "blocks": [
    {
      "type": "product",
      "name": "Product Column",
      "settings": [
        { "type": "text", "id": "name", "label": "Product name", "default": "Basic" },
        { "type": "text", "id": "price", "label": "Price", "default": "$49" },
        {
          "type": "textarea",
          "id": "values",
          "label": "Feature values (pipe-separated)",
          "info": "Match the order of feature rows. Use 'yes' or 'no' for check/cross marks, or type custom text.",
          "default": "8 hours|yes|no|120g"
        },
        { "type": "url", "id": "cta_url", "label": "Buy button link" },
        { "type": "text", "id": "cta_text", "label": "Buy button text", "default": "Buy Now" }
      ]
    }
  ],
  "presets": [
    {
      "name": "Product Comparison",
      "blocks": [
        { "type": "product", "settings": { "name": "Basic", "price": "$49", "values": "8 hours|yes|no|120g" } },
        { "type": "product", "settings": { "name": "Pro", "price": "$79", "values": "12 hours|yes|yes|135g" } },
        { "type": "product", "settings": { "name": "Premium", "price": "$99", "values": "16 hours|yes|yes|140g" } }
      ]
    }
  ]
}
{% endschema %}
{% endraw %}

Customization options: Feature rows and values are pipe-separated for easy editing in the theme editor. Use “yes” or “no” for checkmark/cross icons, or type any custom text. Each product column has its own CTA button. The table scrolls horizontally on mobile.

8. Multi-Column Content with CTA Blocks

Use this for landing pages, about pages, or any page that needs flexible content columns with optional call-to-action buttons. Each column is a block with a heading, rich text, optional image, and optional button. This replaces the need for page builder apps on most stores.

{% raw %}
<style>
  .multi-col { padding: 40px 20px; max-width: 1200px; margin: 0 auto; }
  .multi-col__heading { text-align: center; font-size: 28px; margin-bottom: 32px; }
  .multi-col__grid { display: grid; grid-template-columns: repeat({{ section.settings.columns }}, 1fr); gap: 32px; }
  .multi-col__item { }
  .multi-col__image { width: 100%; height: auto; border-radius: 8px; margin-bottom: 16px; }
  .multi-col__title { font-size: 20px; font-weight: 600; margin-bottom: 8px; color: #111; }
  .multi-col__text { font-size: 15px; line-height: 1.6; color: #444; margin-bottom: 16px; }
  .multi-col__cta { display: inline-block; padding: 10px 24px; background: #111; color: #fff; text-decoration: none; font-size: 14px; font-weight: 600; border-radius: 4px; }
  .multi-col__cta:hover { opacity: 0.85; }
  @media (max-width: 768px) {
    .multi-col__grid { grid-template-columns: 1fr; }
  }
</style>

<section class="multi-col">
  {% if section.settings.heading != blank %}
    <h2 class="multi-col__heading">{{ section.settings.heading }}</h2>
  {% endif %}

  <div class="multi-col__grid">
    {% for block in section.blocks %}
      <div class="multi-col__item" {{ block.shopify_attributes }}>
        {% if block.settings.image != blank %}
          <img class="multi-col__image"
               src="{{ block.settings.image | image_url: width: 800 }}"
               alt="{{ block.settings.title }}"
               loading="lazy">
        {% endif %}
        {% if block.settings.title != blank %}
          <h3 class="multi-col__title">{{ block.settings.title }}</h3>
        {% endif %}
        {% if block.settings.text != blank %}
          <div class="multi-col__text">{{ block.settings.text }}</div>
        {% endif %}
        {% if block.settings.cta_text != blank and block.settings.cta_url != blank %}
          <a class="multi-col__cta" href="{{ block.settings.cta_url }}">{{ block.settings.cta_text }}</a>
        {% endif %}
      </div>
    {% endfor %}
  </div>
</section>

{% schema %}
{
  "name": "Multi-Column Content",
  "settings": [
    { "type": "text", "id": "heading", "label": "Section heading" },
    {
      "type": "range",
      "id": "columns",
      "label": "Columns (desktop)",
      "min": 2,
      "max": 4,
      "step": 1,
      "default": 3
    }
  ],
  "blocks": [
    {
      "type": "column",
      "name": "Column",
      "settings": [
        { "type": "image_picker", "id": "image", "label": "Image" },
        { "type": "text", "id": "title", "label": "Title" },
        { "type": "richtext", "id": "text", "label": "Content" },
        { "type": "text", "id": "cta_text", "label": "Button text" },
        { "type": "url", "id": "cta_url", "label": "Button link" }
      ]
    }
  ],
  "presets": [
    {
      "name": "Multi-Column Content",
      "blocks": [
        { "type": "column", "settings": { "title": "Column One" } },
        { "type": "column", "settings": { "title": "Column Two" } },
        { "type": "column", "settings": { "title": "Column Three" } }
      ]
    }
  ]
}
{% endschema %}
{% endraw %}

Customization options: 2 to 4 columns with responsive stacking. Each column supports an image, rich text (with bold, italic, links), and an optional CTA button. This single section can replace most of what page builder apps like Shogun or GemPages are used for on simple landing pages.

9. Custom Collection Grid with Filters

Use this to display a filtered collection grid with tag-based filtering. Visitors click filter buttons to show products matching specific tags. This is useful for stores that organize products by material, color, use case, or other attributes stored as tags.

For full context on how Liquid handles collections and tag filtering, see my Shopify Liquid development guide.

{% raw %}
<style>
  .filtered-grid { padding: 40px 20px; max-width: 1200px; margin: 0 auto; }
  .filtered-grid__heading { text-align: center; font-size: 28px; margin-bottom: 24px; }
  .filtered-grid__filters { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-bottom: 24px; }
  .filtered-grid__btn { padding: 8px 20px; border: 1px solid #ddd; background: #fff; cursor: pointer; font-size: 14px; border-radius: 20px; transition: all 0.2s; }
  .filtered-grid__btn--active, .filtered-grid__btn:hover { background: #111; color: #fff; border-color: #111; }
  .filtered-grid__products { display: grid; grid-template-columns: repeat({{ section.settings.columns }}, 1fr); gap: 24px; }
  .filtered-grid__product { text-decoration: none; color: inherit; }
  .filtered-grid__img { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 8px; margin-bottom: 8px; }
  .filtered-grid__title { font-size: 15px; font-weight: 600; margin-bottom: 4px; }
  .filtered-grid__price { font-size: 14px; color: #666; }
  .filtered-grid__product[data-hidden="true"] { display: none; }
  @media (max-width: 768px) {
    .filtered-grid__products { grid-template-columns: repeat(2, 1fr); }
  }
</style>

<section class="filtered-grid" id="filtered-grid-{{ section.id }}">
  {% if section.settings.heading != blank %}
    <h2 class="filtered-grid__heading">{{ section.settings.heading }}</h2>
  {% endif %}

  {% assign collection = collections[section.settings.collection] %}

  {% if collection != blank %}
    {% assign all_tags = '' %}
    {% for product in collection.products limit: section.settings.limit %}
      {% for tag in product.tags %}
        {% unless all_tags contains tag %}
          {% if all_tags == '' %}
            {% assign all_tags = tag %}
          {% else %}
            {% assign all_tags = all_tags | append: ',' | append: tag %}
          {% endif %}
        {% endunless %}
      {% endfor %}
    {% endfor %}
    {% assign tag_list = all_tags | split: ',' | sort %}

    <div class="filtered-grid__filters" id="filters-{{ section.id }}">
      <button class="filtered-grid__btn filtered-grid__btn--active" data-filter="all">All</button>
      {% for tag in tag_list %}
        <button class="filtered-grid__btn" data-filter="{{ tag | handleize }}">{{ tag }}</button>
      {% endfor %}
    </div>

    <div class="filtered-grid__products" id="products-{{ section.id }}">
      {% for product in collection.products limit: section.settings.limit %}
        <a class="filtered-grid__product"
           href="{{ product.url }}"
           data-tags="{% for tag in product.tags %}{{ tag | handleize }}{% unless forloop.last %},{% endunless %}{% endfor %}">
          {% if product.featured_image %}
            <img class="filtered-grid__img"
                 src="{{ product.featured_image | image_url: width: 600 }}"
                 alt="{{ product.title }}"
                 loading="lazy"
                 width="300" height="300">
          {% endif %}
          <div class="filtered-grid__title">{{ product.title }}</div>
          <div class="filtered-grid__price">{{ product.price | money }}</div>
        </a>
      {% endfor %}
    </div>
  {% else %}
    <p style="text-align:center;color:#999;">Select a collection in the theme editor.</p>
  {% endif %}
</section>

<script>
  (function() {
    var filters = document.getElementById('filters-{{ section.id }}');
    var products = document.getElementById('products-{{ section.id }}');
    if (!filters || !products) return;

    filters.addEventListener('click', function(e) {
      var btn = e.target.closest('.filtered-grid__btn');
      if (!btn) return;
      var filter = btn.dataset.filter;
      filters.querySelectorAll('.filtered-grid__btn').forEach(function(b) {
        b.classList.remove('filtered-grid__btn--active');
      });
      btn.classList.add('filtered-grid__btn--active');
      products.querySelectorAll('.filtered-grid__product').forEach(function(p) {
        if (filter === 'all') {
          p.dataset.hidden = 'false';
        } else {
          var tags = p.dataset.tags.split(',');
          p.dataset.hidden = tags.indexOf(filter) === -1 ? 'true' : 'false';
        }
      });
    });
  })();
</script>

{% schema %}
{
  "name": "Filtered Collection Grid",
  "settings": [
    { "type": "text", "id": "heading", "label": "Heading" },
    { "type": "collection", "id": "collection", "label": "Collection" },
    {
      "type": "range",
      "id": "columns",
      "label": "Columns (desktop)",
      "min": 2,
      "max": 5,
      "step": 1,
      "default": 4
    },
    {
      "type": "range",
      "id": "limit",
      "label": "Max products",
      "min": 4,
      "max": 50,
      "step": 2,
      "default": 20
    }
  ],
  "presets": [
    { "name": "Filtered Collection Grid" }
  ]
}
{% endschema %}
{% endraw %}

Customization options: Select any collection from the editor. Set the number of columns and max products. Filters are auto-generated from product tags. The “All” button shows every product. Products hide and show instantly with no page reload.

10. Sticky Add-to-Cart Bar

Use this on product pages to keep the add-to-cart button visible as visitors scroll down through long product descriptions, reviews, and other content. The bar appears when the main add-to-cart button scrolls out of view and disappears when it scrolls back in.

I covered the conversion impact of sticky add-to-cart on mobile in my sticky add-to-cart guide. On one client store, adding this section increased mobile add-to-cart rate by 12%. For the full context on how this was built and measured with Clarity data, read that post.

{% raw %}
<style>
  .sticky-atc { position: fixed; bottom: 0; left: 0; right: 0; background: #fff; box-shadow: 0 -2px 10px rgba(0,0,0,0.1); padding: 12px 20px; display: flex; align-items: center; justify-content: space-between; gap: 12px; z-index: 999; transform: translateY(100%); transition: transform 0.3s ease; }
  .sticky-atc--visible { transform: translateY(0); }
  .sticky-atc__info { display: flex; align-items: center; gap: 12px; flex: 1; min-width: 0; }
  .sticky-atc__image { width: 48px; height: 48px; object-fit: cover; border-radius: 4px; flex-shrink: 0; }
  .sticky-atc__details { min-width: 0; }
  .sticky-atc__title { font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .sticky-atc__price { font-size: 14px; color: #666; }
  .sticky-atc__button { padding: 12px 32px; background: #111; color: #fff; border: none; font-size: 16px; font-weight: 600; cursor: pointer; border-radius: 4px; flex-shrink: 0; white-space: nowrap; }
  .sticky-atc__button:hover { opacity: 0.9; }
  @media (min-width: 769px) {
    .sticky-atc { padding: 12px 40px; }
  }
</style>

{% if product != blank %}
  <div class="sticky-atc" id="sticky-atc-{{ section.id }}">
    <div class="sticky-atc__info">
      {% if product.featured_image %}
        <img class="sticky-atc__image"
             src="{{ product.featured_image | image_url: width: 96 }}"
             alt="{{ product.title }}"
             width="48" height="48">
      {% endif %}
      <div class="sticky-atc__details">
        <div class="sticky-atc__title">{{ product.title }}</div>
        <div class="sticky-atc__price">{{ product.selected_or_first_available_variant.price | money }}</div>
      </div>
    </div>
    <button class="sticky-atc__button" id="sticky-atc-btn-{{ section.id }}">
      {{ section.settings.button_text | default: 'Add to Cart' }}
    </button>
  </div>

  <script>
    (function() {
      var bar = document.getElementById('sticky-atc-{{ section.id }}');
      var btn = document.getElementById('sticky-atc-btn-{{ section.id }}');
      if (!bar) return;

      var mainBtn = document.querySelector('form[action="/cart/add"] [type="submit"], .product-form__submit, button[name="add"]');

      function checkVisibility() {
        if (!mainBtn) {
          bar.classList.add('sticky-atc--visible');
          return;
        }
        var rect = mainBtn.getBoundingClientRect();
        var isVisible = rect.top < window.innerHeight && rect.bottom > 0;
        bar.classList.toggle('sticky-atc--visible', !isVisible);
      }

      window.addEventListener('scroll', checkVisibility, { passive: true });
      checkVisibility();

      btn.addEventListener('click', function() {
        if (mainBtn) {
          mainBtn.click();
        } else {
          var variantId = {{ product.selected_or_first_available_variant.id }};
          fetch('/cart/add.js', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ items: [{ id: variantId, quantity: 1 }] })
          }).then(function() {
            window.location.href = '/cart';
          });
        }
      });
    })();
  </script>
{% endif %}

{% schema %}
{
  "name": "Sticky Add to Cart",
  "settings": [
    { "type": "text", "id": "button_text", "label": "Button text", "default": "Add to Cart" }
  ],
  "templates": ["product"],
  "presets": [
    { "name": "Sticky Add to Cart" }
  ]
}
{% endschema %}
{% endraw %}

Customization options: Button text is editable from the theme editor. The bar auto-detects the main add-to-cart button on the page and only shows when it is not visible. It works with both standard Shopify forms and AJAX cart implementations. The section is template-restricted to product pages only.

How to Install Custom Sections in Your Shopify Theme

Adding a custom section to your theme takes five steps: duplicate your theme, create the file, paste the code, preview in the editor, and publish. For a complete walkthrough of safe theme editing practices, see my Shopify theme customization guide.

Step 1: Duplicate your live theme. Never edit a published theme directly. Go to Online Store > Themes, click the three dots on your current theme, and select Duplicate. Work on the copy.

Step 2: Open the code editor. On your duplicated theme, click the three dots and select Edit code. Navigate to the sections/ directory.

Step 3: Create a new file. Click “Add a new section” and give it a descriptive name like custom-faq-accordion.liquid. Paste the entire code block from the section you want, including the style tag, the Liquid template, the script tag, and the schema block.

Step 4: Preview and configure. Go back to the theme editor (Customize), navigate to the page where you want the section, and click “Add section.” Your new section will appear in the list by its preset name. Add it, configure the settings, and preview across desktop, tablet, and mobile.

Step 5: Publish. Once you have confirmed everything works correctly, publish the duplicated theme to make it live.

If you need to use these sections in multiple templates (homepage, product page, landing pages), Online Store 2.0 lets you add sections to any JSON template. For sections restricted to specific templates (like the Sticky Add to Cart), the schema includes a templates key that limits where the section appears.

For more patterns on structuring your theme architecture, read my Shopify Liquid development guide.

Frequently Asked Questions

What is a Shopify Liquid section?

A Shopify Liquid section is a modular, customizable component of your theme that merchants can add, remove, and configure from the theme editor. Each section has two parts: the Liquid template that renders the HTML, and a schema JSON block that defines the settings and blocks available in the editor. Online Store 2.0 themes support sections on every page, not just the homepage.

Can I add custom sections to any Shopify theme?

Yes. Any Online Store 2.0 theme supports custom sections on all pages. Create a new .liquid file in your theme’s sections/ directory, include a valid schema block, and the section becomes available in the theme editor via its preset. Vintage themes only support sections on the homepage, so you would need to upgrade to OS 2.0 first.

Do custom sections slow down my Shopify store?

Custom Liquid sections are server-rendered, meaning Shopify processes them before sending HTML to the browser. They add virtually zero client-side overhead compared to app-based alternatives that inject JavaScript. The sections in this guide use minimal inline CSS and only include JavaScript where strictly necessary, like the accordion toggle or countdown timer.

How do I test custom sections before publishing?

Always duplicate your live theme first and work on the copy. Add your section file, then preview it from the theme editor. Test across desktop, tablet, and mobile. Check the browser console for any JavaScript errors. Only publish the theme after confirming everything works correctly. For a full safe-editing workflow, see my Shopify theme customization guide.

What is the difference between sections and snippets in Shopify?

Sections are standalone components with their own schema settings that merchants can manage from the theme editor. They live in the sections/ directory and can be added to any page template in OS 2.0 themes. Snippets are reusable code fragments stored in the snippets/ directory that you include with the render tag. Snippets have no schema and are not directly accessible from the theme editor. Use sections when merchants need to configure the component. Use snippets for shared logic or markup that only developers need to manage. For deeper coverage of Liquid architecture, read my Shopify Liquid snippets guide.


Need Custom Sections Built for Your Store?

Every store I audit has at least 3-5 sections that could be custom-built to match their exact brand and conversion goals, replacing generic theme defaults and bloated apps. The sections above are starting points, but the real wins come from sections designed around your specific products, audience, and data.

View my services or book a free strategy call to discuss what custom sections could do for your store.

Need a Liquid Developer Who Understands CRO?

I'll audit your theme code and show you exactly what's costing you conversions. 12+ years of Shopify Liquid experience across 100+ stores.

Get a Free Code Review

Frequently Asked Questions

What is a Shopify Liquid section?

A Shopify Liquid section is a modular, customizable component of your theme that merchants can add, remove, and configure from the theme editor. Each section has two parts: the Liquid template that renders the HTML, and a schema JSON block that defines the settings and blocks available in the editor. Online Store 2.0 themes support sections on every page, not just the homepage.

Can I add custom sections to any Shopify theme?

Yes. Any Online Store 2.0 theme supports custom sections on all pages. Create a new .liquid file in your theme's sections/ directory, include a valid schema block, and the section becomes available in the theme editor via its preset. Vintage themes only support sections on the homepage, so you would need to upgrade to OS 2.0 first.

Do custom sections slow down my Shopify store?

Custom Liquid sections are server-rendered, meaning Shopify processes them before sending HTML to the browser. They add virtually zero client-side overhead compared to app-based alternatives that inject JavaScript. The sections in this guide use minimal inline CSS and only include JavaScript where strictly necessary, like the accordion toggle or countdown timer.

How do I test custom sections before publishing?

Always duplicate your live theme first and work on the copy. Add your section file, then preview it from the theme editor. Test across desktop, tablet, and mobile. Check the browser console for any JavaScript errors. Only publish the theme after confirming everything works correctly. For a full safe-editing workflow, see my Shopify theme customization guide.

What is the difference between sections and snippets in Shopify?

Sections are standalone components with their own schema settings that merchants can manage from the theme editor. They live in the sections/ directory and can be added to any page template in OS 2.0 themes. Snippets are reusable code fragments stored in the snippets/ directory that you include with the render tag. Snippets have no schema and are not directly accessible from the theme editor. Use sections when merchants need to configure the component. Use snippets for shared logic or markup that only developers need to manage.