Talk Shop
Home
Learn More
About Us
Follow Us
Blog
Tools
Newsletter
Join Discord
Join

Community

  • Developers
  • Growth
  • Entrepreneurs
  • Support
  • Experts
  • Tools

Location

123 Mars, Crater City, Red Planet

(WiFi may be spotty)

Hours

Who has time for breaks? We're here 24/7!

Contact

hello@letstalkshop.com

Talk Shop
Talk Shop

Built for real builders. Not affiliated with Shopify Inc.

Home
Privacy
Terms
  1. Home
  2. >Blog
  3. >Theme Design
  4. >Shopify Editable Sections Without Apps: Build Custom Sections Natively
Theme Design11 min read

Shopify Editable Sections Without Apps: Build Custom Sections Natively

Stop paying for page builder apps. Learn how to build fully customizable Shopify sections using native Liquid, section schema settings, and blocks — no apps required.

Talk Shop

Talk Shop

Mar 18, 2026

Shopify Editable Sections Without Apps: Build Custom Sections Natively

In this article

  • Page Builder Apps Are a Tax on Your Store's Performance and Profit
  • Why Native Sections Beat Page Builder Apps
  • Understanding Section Schema: The Foundation
  • Every Setting Type You Need to Know
  • Blocks: Repeatable Content Without Limits
  • Real-World Section: Testimonials Carousel
  • Real-World Section: FAQ Accordion With Schema Markup
  • Real-World Section: Feature Grid With Icons
  • Real-World Section: CTA Banner With Dynamic Product
  • Dynamic Sources: Connecting Sections to Metafields
  • When to Keep a Page Builder — And How to Migrate Off One
  • Build Your Section Library Today

Page Builder Apps Are a Tax on Your Store's Performance and Profit

PageFly charges $99/month. GemPages charges $79/month. Shogun charges $99/month. Over a year, that's $948 to $1,188 for functionality that Shopify already provides for free through native section schemas. Worse, every one of these apps injects external JavaScript, adds DOM elements, and increases your page weight — all of which directly hurt your Core Web Vitals scores and your conversion rate.

Building Shopify editable sections without apps is not a compromise. It's an upgrade. Native sections load faster, integrate seamlessly with the theme editor, survive theme updates without breaking, and give merchants the exact same drag-and-drop editing experience they're paying those apps for.

This guide gives you the complete toolkit: every setting type available, blocks for repeatable content, presets for discoverability, dynamic sources with metafields, and four production-ready section examples you can paste into your theme today. If you're comfortable editing a .liquid file, you already have everything you need.

Why Native Sections Beat Page Builder Apps

The argument for page builder apps usually boils down to "merchants need visual editing without code." But that's exactly what Shopify's native section architecture already provides. Let's compare.

Performance

Page builder apps wrap your content in extra <div> layers, load their own CSS frameworks, inject JavaScript for rendering, and sometimes make additional API calls on page load. A Shogun-built page can add 200-500KB of JavaScript. A native section adds zero — your Liquid renders server-side into clean HTML.

Cost

ApproachMonthly CostAnnual Cost3-Year Cost
PageFly Pro$99/mo$1,188$3,564
GemPages Pro$79/mo$948$2,844
Shogun Advanced$99/mo$1,188$3,564
Native Sections$0$0$0

The upfront time investment to learn section schemas pays for itself within the first month.

Theme Editor Integration

Native sections are first-class citizens in the Shopify theme editor. They support real-time preview, inline editing, drag-and-drop reordering, undo/redo, and version history. Page builder apps create their own editing experience that sits alongside (and sometimes conflicts with) the native editor. When Shopify ships editor improvements, native sections get them automatically. App-built pages don't.

Maintainability

When you update your theme, native sections update with it. When you uninstall a page builder app, every page built with it breaks. That lock-in is by design — it's how these apps retain customers. Native sections have zero dependency on any third party.

According to Shopify's theme architecture documentation, sections are the primary building blocks of Online Store 2.0 themes, and the schema system was specifically designed to give merchants full visual control without requiring code changes.

Understanding Section Schema: The Foundation

Holographic code structure showing JSON schema hierarchy.

Every editable section in Shopify has two parts: the Liquid/HTML template at the top, and a {% schema %} JSON block at the bottom. The schema is what creates the editing interface merchants see in the theme editor. No schema means no editing UI.

Here's the minimal structure:

liquidliquid
<section class="my-section">
  <h2>{{ section.settings.heading }}</h2>
</section>

{% schema %}
{
  "name": "My Section",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Default heading text"
    }
  ],
  "presets": [
    {
      "name": "My Section"
    }
  ]
}
{% endschema %}

Three things make this work:

  1. `name` — What merchants see in the editor sidebar when they select this section.
  2. `settings` — An array of input fields. Each setting has a type, a unique id, a label, and an optional default value. The merchant's input is accessible via section.settings.<id>.
  3. `presets` — Makes the section appear in the "Add section" picker. Without presets, the section exists in your codebase but merchants cannot add it to pages.

If you're new to Liquid and theme development, our beginner's guide to Shopify theme development covers the full environment setup and fundamentals.

Every Setting Type You Need to Know

Holographic visualization of different Shopify section setting types.

Shopify provides a rich set of setting types that cover virtually every input a merchant might need. Understanding what's available eliminates the "I need an app for that" reflex.

Basic Input Settings

Setting TypeEditor UIUse CaseAccess in Liquid
textSingle-line text fieldHeadings, labels, short text{{ section.settings.id }}
textareaMulti-line text areaDescriptions, paragraphs{{ section.settings.id }}
richtextRich text editor (bold, italic, links)Formatted body content{{ section.settings.id }}
inline_richtextInline rich text (bold, italic only)Headings with emphasis{{ section.settings.id }}
numberNumber inputCounts, quantities{{ section.settings.id }}
checkboxToggle switchShow/hide elements{% if section.settings.id %}

Media Settings

Setting TypeEditor UIUse Case
image_pickerImage upload/select dialogHero images, thumbnails, icons
videoVideo file picker (hosted)Background videos, product demos
video_urlURL input (YouTube/Vimeo)Embedded third-party video

Resource Picker Settings

These let merchants link sections to Shopify resources:

jsonjson
{
  "type": "collection",
  "id": "featured_collection",
  "label": "Featured collection"
}

Available resource types: collection, product, product_list, collection_list, blog, page, article, url, link_list (navigation menus).

Access them naturally in Liquid:

liquidliquid
{%- assign collection = section.settings.featured_collection -%}
{%- for product in collection.products limit: 4 -%}
  <a href="{{ product.url }}">{{ product.title }} — {{ product.price | money }}</a>
{%- endfor -%}

Design Control Settings

These are the settings that replace what page builders charge for:

jsonjson
[
  {
    "type": "color",
    "id": "background_color",
    "label": "Background color",
    "default": "#ffffff"
  },
  {
    "type": "color_background",
    "id": "gradient",
    "label": "Background gradient"
  },
  {
    "type": "range",
    "id": "padding_top",
    "min": 0,
    "max": 100,
    "step": 4,
    "unit": "px",
    "label": "Top padding",
    "default": 40
  },
  {
    "type": "select",
    "id": "layout",
    "label": "Layout style",
    "options": [
      { "value": "contained", "label": "Contained" },
      { "value": "full-width", "label": "Full width" }
    ],
    "default": "contained"
  },
  {
    "type": "font_picker",
    "id": "heading_font",
    "label": "Heading font",
    "default": "helvetica_n4"
  }
]

The range type alone replaces the padding/margin sliders that page builder apps advertise as premium features. The color and color_background types cover solid colors and CSS gradients. The select type handles any enumerated choice.

For a full reference of every setting type with examples, see Shopify's input settings documentation.

Blocks: Repeatable Content Without Limits

Holographic visualization of dynamic repeating content blocks.

Settings handle section-level configuration. Blocks handle repeatable content within a section — individual testimonials, FAQ items, feature cards, team members, image slides. Blocks are what make sections truly dynamic.

Each block has its own type, its own settings, and merchants can add, remove, and reorder blocks through the theme editor. This is the same functionality that page builder apps market as "drag-and-drop content modules."

How Blocks Work

Blocks are defined in the blocks array inside your schema:

jsonjson
{
  "name": "Feature Grid",
  "settings": [],
  "blocks": [
    {
      "type": "feature",
      "name": "Feature",
      "settings": [
        {
          "type": "image_picker",
          "id": "icon",
          "label": "Icon"
        },
        {
          "type": "text",
          "id": "title",
          "label": "Title"
        },
        {
          "type": "textarea",
          "id": "description",
          "label": "Description"
        }
      ]
    }
  ],
  "presets": [
    {
      "name": "Feature Grid",
      "blocks": [
        { "type": "feature" },
        { "type": "feature" },
        { "type": "feature" }
      ]
    }
  ]
}

In the Liquid template, you iterate over blocks:

liquidliquid
{%- for block in section.blocks -%}
  <div class="feature-card" {{ block.shopify_attributes }}>
    {%- if block.settings.icon -%}
      <img src="{{ block.settings.icon | image_url: width: 120 }}" alt="" width="60" height="60" loading="lazy">
    {%- endif -%}
    <h3>{{ block.settings.title }}</h3>
    <p>{{ block.settings.description }}</p>
  </div>
{%- endfor -%}

The {{ block.shopify_attributes }} tag is critical — it connects each rendered block to the theme editor so merchants can click on a specific block in the preview and edit it directly. Leave it out and the editor highlighting won't work.

Multiple Block Types in One Section

A single section can support multiple block types. This lets you build flexible layouts where merchants mix different content modules:

jsonjson
"blocks": [
  {
    "type": "heading",
    "name": "Heading",
    "settings": [
      { "type": "inline_richtext", "id": "text", "label": "Heading text" }
    ]
  },
  {
    "type": "text",
    "name": "Text",
    "settings": [
      { "type": "richtext", "id": "text", "label": "Body text" }
    ]
  },
  {
    "type": "image",
    "name": "Image",
    "settings": [
      { "type": "image_picker", "id": "image", "label": "Image" }
    ]
  },
  {
    "type": "button",
    "name": "Button",
    "settings": [
      { "type": "text", "id": "label", "label": "Button label" },
      { "type": "url", "id": "link", "label": "Button link" }
    ]
  }
]

In your Liquid, use block.type to render each one differently:

liquidliquid
{%- for block in section.blocks -%}
  {%- case block.type -%}
    {%- when 'heading' -%}
      <h2 {{ block.shopify_attributes }}>{{ block.settings.text }}</h2>
    {%- when 'text' -%}
      <div {{ block.shopify_attributes }}>{{ block.settings.text }}</div>
    {%- when 'image' -%}
      <img {{ block.shopify_attributes }} src="{{ block.settings.image | image_url: width: 800 }}" alt="{{ block.settings.image.alt | escape }}" loading="lazy">
    {%- when 'button' -%}
      <a {{ block.shopify_attributes }} href="{{ block.settings.link }}" class="button">{{ block.settings.label }}</a>
  {%- endcase -%}
{%- endfor -%}

This pattern — a single section accepting multiple block types — is the native equivalent of a page builder's "content column" or "flexible content" widget. Except it renders as clean, server-side HTML.

To understand how blocks interact with app extensions, see our breakdown of Shopify app blocks vs theme sections.

Real-World Section: Testimonials Carousel

Let's build a production-ready testimonials section with star ratings, author info, avatars, and merchant controls for layout and styling.

Create sections/testimonials-native.liquid:

liquidliquid
{{ 'section-testimonials.css' | asset_url | stylesheet_tag }}

<section
  class="testimonials-native"
  style="
    --testimonials-bg: {{ section.settings.background_color }};
    --testimonials-text: {{ section.settings.text_color }};
    --testimonials-columns: {{ section.settings.columns }};
    padding-top: {{ section.settings.padding_top }}px;
    padding-bottom: {{ section.settings.padding_bottom }}px;
  "
>
  <div class="testimonials-native__container">
    {%- if section.settings.heading != blank -%}
      <h2 class="testimonials-native__heading">{{ section.settings.heading }}</h2>
    {%- endif -%}
    {%- if section.settings.subheading != blank -%}
      <p class="testimonials-native__subheading">{{ section.settings.subheading }}</p>
    {%- endif -%}

    <div class="testimonials-native__grid">
      {%- for block in section.blocks -%}
        <div class="testimonials-native__card" {{ block.shopify_attributes }}>
          {%- if block.settings.rating > 0 -%}
            <div class="testimonials-native__stars" aria-label="{{ block.settings.rating }} out of 5 stars">
              {%- for i in (1..5) -%}
                <span class="testimonials-native__star {% if i <= block.settings.rating %}testimonials-native__star--filled{% endif %}">★</span>
              {%- endfor -%}
            </div>
          {%- endif -%}

          {%- if block.settings.quote != blank -%}
            <blockquote class="testimonials-native__quote">{{ block.settings.quote }}</blockquote>
          {%- endif -%}

          <div class="testimonials-native__author">
            {%- if block.settings.avatar -%}
              <img
                src="{{ block.settings.avatar | image_url: width: 96 }}"
                alt="{{ block.settings.name | escape }}"
                width="48"
                height="48"
                class="testimonials-native__avatar"
                loading="lazy"
              >
            {%- endif -%}
            <div class="testimonials-native__author-info">
              {%- if block.settings.name != blank -%}
                <strong>{{ block.settings.name }}</strong>
              {%- endif -%}
              {%- if block.settings.title != blank -%}
                <span class="testimonials-native__role">{{ block.settings.title }}</span>
              {%- endif -%}
            </div>
          </div>
        </div>
      {%- endfor -%}
    </div>
  </div>
</section>

{% schema %}
{
  "name": "Testimonials",
  "tag": "section",
  "class": "testimonials-section",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "What Our Customers Say"
    },
    {
      "type": "text",
      "id": "subheading",
      "label": "Subheading"
    },
    {
      "type": "range",
      "id": "columns",
      "min": 1,
      "max": 4,
      "step": 1,
      "default": 3,
      "label": "Columns per row"
    },
    {
      "type": "color",
      "id": "background_color",
      "label": "Background color",
      "default": "#f9fafb"
    },
    {
      "type": "color",
      "id": "text_color",
      "label": "Text color",
      "default": "#1a1a1a"
    },
    {
      "type": "range",
      "id": "padding_top",
      "min": 0,
      "max": 100,
      "step": 4,
      "unit": "px",
      "default": 60,
      "label": "Top padding"
    },
    {
      "type": "range",
      "id": "padding_bottom",
      "min": 0,
      "max": 100,
      "step": 4,
      "unit": "px",
      "default": 60,
      "label": "Bottom padding"
    }
  ],
  "blocks": [
    {
      "type": "testimonial",
      "name": "Testimonial",
      "settings": [
        {
          "type": "range",
          "id": "rating",
          "min": 0,
          "max": 5,
          "step": 1,
          "default": 5,
          "label": "Star rating"
        },
        {
          "type": "textarea",
          "id": "quote",
          "label": "Quote",
          "default": "This product changed everything for our business."
        },
        {
          "type": "text",
          "id": "name",
          "label": "Author name",
          "default": "Jane Smith"
        },
        {
          "type": "text",
          "id": "title",
          "label": "Author title",
          "default": "Founder, Acme Co."
        },
        {
          "type": "image_picker",
          "id": "avatar",
          "label": "Author photo"
        }
      ]
    }
  ],
  "presets": [
    {
      "name": "Testimonials",
      "blocks": [
        { "type": "testimonial" },
        { "type": "testimonial" },
        { "type": "testimonial" }
      ]
    }
  ]
}
{% endschema %}

The CSS for this section goes in assets/section-testimonials.css:

csscss
.testimonials-native {
  background: var(--testimonials-bg, #f9fafb);
  color: var(--testimonials-text, #1a1a1a);
}

.testimonials-native__container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 1.5rem;
}

.testimonials-native__heading {
  font-size: clamp(1.5rem, 3vw, 2.25rem);
  text-align: center;
  margin-bottom: 0.5rem;
}

.testimonials-native__subheading {
  text-align: center;
  opacity: 0.7;
  margin-bottom: 2.5rem;
}

.testimonials-native__grid {
  display: grid;
  grid-template-columns: repeat(var(--testimonials-columns, 3), 1fr);
  gap: 1.5rem;
}

@media (max-width: 768px) {
  .testimonials-native__grid {
    grid-template-columns: 1fr;
  }
}

.testimonials-native__card {
  background: #fff;
  border-radius: 8px;
  padding: 1.5rem;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}

.testimonials-native__stars { margin-bottom: 0.75rem; }
.testimonials-native__star { color: #d1d5db; font-size: 1.1rem; }
.testimonials-native__star--filled { color: #f59e0b; }

.testimonials-native__quote {
  font-size: 0.95rem;
  line-height: 1.6;
  margin: 0 0 1.25rem;
  font-style: italic;
}

.testimonials-native__author {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.testimonials-native__avatar {
  border-radius: 50%;
  object-fit: cover;
}

.testimonials-native__role {
  display: block;
  font-size: 0.85rem;
  opacity: 0.6;
}

Merchants can add unlimited testimonials, reorder them, control the column count, and adjust colors and spacing — all through the theme editor. Zero apps needed.

Real-World Section: FAQ Accordion With Schema Markup

FAQ sections are one of the top reasons merchants install page builder apps. A native version with proper <details> elements and JSON-LD schema markup outperforms any app-generated FAQ in both UX and SEO.

Create sections/faq-native.liquid:

liquidliquid
<section
  class="faq-native"
  style="padding-top: {{ section.settings.padding_top }}px; padding-bottom: {{ section.settings.padding_bottom }}px;"
>
  <div class="faq-native__container">
    {%- if section.settings.heading != blank -%}
      <h2 class="faq-native__heading">{{ section.settings.heading }}</h2>
    {%- endif -%}

    <div class="faq-native__list">
      {%- for block in section.blocks -%}
        <details class="faq-native__item" {{ block.shopify_attributes }}>
          <summary class="faq-native__question">
            {{ block.settings.question }}
            <svg class="faq-native__icon" width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
              <path d="M5 7.5L10 12.5L15 7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
            </svg>
          </summary>
          <div class="faq-native__answer">
            {{ block.settings.answer }}
          </div>
        </details>
      {%- endfor -%}
    </div>
  </div>
</section>

{%- if section.blocks.size > 0 -%}
  <script type="application/ld+json">
    {
      "@context": "https://schema.org",
      "@type": "FAQPage",
      "mainEntity": [
        {%- for block in section.blocks -%}
          {
            "@type": "Question",
            "name": {{ block.settings.question | json }},
            "acceptedAnswer": {
              "@type": "Answer",
              "text": {{ block.settings.answer | strip_html | json }}
            }
          }{%- unless forloop.last -%},{%- endunless -%}
        {%- endfor -%}
      ]
    }
  </script>
{%- endif -%}

<style>
  .faq-native__container {
    max-width: 800px;
    margin: 0 auto;
    padding: 0 1.5rem;
  }
  .faq-native__heading {
    font-size: clamp(1.5rem, 3vw, 2.25rem);
    text-align: center;
    margin-bottom: 2rem;
  }
  .faq-native__item {
    border-bottom: 1px solid #e5e7eb;
  }
  .faq-native__question {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1.25rem 0;
    font-weight: 600;
    font-size: 1.05rem;
    cursor: pointer;
    list-style: none;
  }
  .faq-native__question::-webkit-details-marker { display: none; }
  .faq-native__icon {
    transition: transform 0.2s ease;
    flex-shrink: 0;
    margin-left: 1rem;
  }
  .faq-native__item[open] .faq-native__icon {
    transform: rotate(180deg);
  }
  .faq-native__answer {
    padding: 0 0 1.25rem;
    line-height: 1.7;
    color: #4b5563;
  }
</style>

{% schema %}
{
  "name": "FAQ",
  "tag": "section",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Frequently Asked Questions"
    },
    {
      "type": "range",
      "id": "padding_top",
      "min": 0,
      "max": 100,
      "step": 4,
      "unit": "px",
      "default": 60,
      "label": "Top padding"
    },
    {
      "type": "range",
      "id": "padding_bottom",
      "min": 0,
      "max": 100,
      "step": 4,
      "unit": "px",
      "default": 60,
      "label": "Bottom padding"
    }
  ],
  "blocks": [
    {
      "type": "faq_item",
      "name": "FAQ Item",
      "settings": [
        {
          "type": "text",
          "id": "question",
          "label": "Question",
          "default": "What is your return policy?"
        },
        {
          "type": "richtext",
          "id": "answer",
          "label": "Answer",
          "default": "<p>We offer a 30-day return policy on all items. Contact our support team to initiate a return.</p>"
        }
      ]
    }
  ],
  "presets": [
    {
      "name": "FAQ",
      "blocks": [
        { "type": "faq_item" },
        { "type": "faq_item" },
        { "type": "faq_item" }
      ]
    }
  ]
}
{% endschema %}

This section does three things no page builder FAQ can match:

  1. Native `<details>/<summary>` — Accessible, keyboard-navigable, no JavaScript required for the accordion behavior.
  2. FAQPage JSON-LD — Automatically generates schema markup for SEO, giving your FAQ items a chance to appear as rich results in Google.
  3. Zero JavaScript — The entire section is HTML and CSS. Page builder FAQ widgets typically load 20-50KB of JavaScript for the same accordion functionality.

Real-World Section: Feature Grid With Icons

Feature grids appear on nearly every Shopify store — "Free Shipping," "24/7 Support," "30-Day Returns." Here's a native section with flexible columns and icon support.

Create sections/feature-grid-native.liquid:

liquidliquid
<section
  class="feature-grid"
  style="
    --fg-columns: {{ section.settings.columns }};
    --fg-bg: {{ section.settings.background_color }};
    padding-top: {{ section.settings.padding_top }}px;
    padding-bottom: {{ section.settings.padding_bottom }}px;
  "
>
  <div class="feature-grid__container">
    {%- 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 }}>
          {%- if block.settings.icon -%}
            <img
              src="{{ block.settings.icon | image_url: width: 120 }}"
              alt=""
              width="60"
              height="60"
              loading="lazy"
              class="feature-grid__icon"
            >
          {%- endif -%}
          {%- if block.settings.title != blank -%}
            <h3 class="feature-grid__title">{{ block.settings.title }}</h3>
          {%- endif -%}
          {%- if block.settings.description != blank -%}
            <p class="feature-grid__description">{{ block.settings.description }}</p>
          {%- endif -%}
          {%- if block.settings.link_label != blank and block.settings.link_url != blank -%}
            <a href="{{ block.settings.link_url }}" class="feature-grid__link">{{ block.settings.link_label }} →</a>
          {%- endif -%}
        </div>
      {%- endfor -%}
    </div>
  </div>
</section>

<style>
  .feature-grid {
    background: var(--fg-bg, #ffffff);
  }
  .feature-grid__container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 1.5rem;
  }
  .feature-grid__heading {
    font-size: clamp(1.5rem, 3vw, 2.25rem);
    text-align: center;
    margin-bottom: 2.5rem;
  }
  .feature-grid__items {
    display: grid;
    grid-template-columns: repeat(var(--fg-columns, 3), 1fr);
    gap: 2rem;
  }
  @media (max-width: 768px) {
    .feature-grid__items {
      grid-template-columns: 1fr;
    }
  }
  .feature-grid__item {
    text-align: center;
  }
  .feature-grid__icon {
    margin: 0 auto 1rem;
    display: block;
  }
  .feature-grid__title {
    font-size: 1.1rem;
    margin-bottom: 0.5rem;
  }
  .feature-grid__description {
    font-size: 0.95rem;
    line-height: 1.6;
    opacity: 0.8;
    margin-bottom: 0.75rem;
  }
  .feature-grid__link {
    font-size: 0.9rem;
    font-weight: 600;
    color: inherit;
    text-decoration: underline;
    text-underline-offset: 3px;
  }
</style>

{% schema %}
{
  "name": "Feature Grid",
  "tag": "section",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Why Choose Us"
    },
    {
      "type": "range",
      "id": "columns",
      "min": 2,
      "max": 4,
      "step": 1,
      "default": 3,
      "label": "Columns"
    },
    {
      "type": "color",
      "id": "background_color",
      "label": "Background color",
      "default": "#ffffff"
    },
    {
      "type": "range",
      "id": "padding_top",
      "min": 0,
      "max": 100,
      "step": 4,
      "unit": "px",
      "default": 60,
      "label": "Top padding"
    },
    {
      "type": "range",
      "id": "padding_bottom",
      "min": 0,
      "max": 100,
      "step": 4,
      "unit": "px",
      "default": 60,
      "label": "Bottom padding"
    }
  ],
  "blocks": [
    {
      "type": "feature",
      "name": "Feature",
      "settings": [
        {
          "type": "image_picker",
          "id": "icon",
          "label": "Icon image"
        },
        {
          "type": "text",
          "id": "title",
          "label": "Title",
          "default": "Free Shipping"
        },
        {
          "type": "textarea",
          "id": "description",
          "label": "Description",
          "default": "Free shipping on all orders over $50."
        },
        {
          "type": "text",
          "id": "link_label",
          "label": "Link label"
        },
        {
          "type": "url",
          "id": "link_url",
          "label": "Link URL"
        }
      ]
    }
  ],
  "presets": [
    {
      "name": "Feature Grid",
      "blocks": [
        { "type": "feature" },
        { "type": "feature" },
        { "type": "feature" }
      ]
    }
  ]
}
{% endschema %}

Each feature block gives merchants control over icon, title, description, and an optional link. The column count is adjustable from the section-level settings. On mobile, the grid collapses to a single column automatically.

Real-World Section: CTA Banner With Dynamic Product

CTA banners are high-value conversion sections. This version lets merchants either enter a custom URL or pick a specific product, and it dynamically pulls the product price and image.

Create sections/cta-banner-native.liquid:

liquidliquid
{%- assign product = section.settings.product -%}

<section
  class="cta-banner"
  style="
    --cta-bg: {{ section.settings.background_color }};
    --cta-text: {{ section.settings.text_color }};
    --cta-btn-bg: {{ section.settings.button_color }};
    --cta-btn-text: {{ section.settings.button_text_color }};
    padding-top: {{ section.settings.padding_top }}px;
    padding-bottom: {{ section.settings.padding_bottom }}px;
  "
>
  <div class="cta-banner__container">
    {%- if section.settings.image -%}
      <div class="cta-banner__media">
        <img
          src="{{ section.settings.image | image_url: width: 600 }}"
          srcset="
            {{ section.settings.image | image_url: width: 400 }} 400w,
            {{ section.settings.image | image_url: width: 600 }} 600w
          "
          sizes="(max-width: 768px) 100vw, 40vw"
          alt="{{ section.settings.image.alt | escape }}"
          loading="lazy"
          width="600"
          height="{{ 600 | divided_by: section.settings.image.aspect_ratio | round }}"
        >
      </div>
    {%- endif -%}

    <div class="cta-banner__content">
      {%- if section.settings.overline != blank -%}
        <p class="cta-banner__overline">{{ section.settings.overline }}</p>
      {%- endif -%}

      {%- if section.settings.heading != blank -%}
        <h2 class="cta-banner__heading">{{ section.settings.heading }}</h2>
      {%- endif -%}

      {%- if section.settings.description != blank -%}
        <div class="cta-banner__description">{{ section.settings.description }}</div>
      {%- endif -%}

      {%- if product -%}
        <p class="cta-banner__price">
          Starting at {{ product.price | money }}
        </p>
      {%- endif -%}

      {%- if section.settings.button_label != blank -%}
        {%- assign button_url = section.settings.button_url -%}
        {%- if product and button_url == blank -%}
          {%- assign button_url = product.url -%}
        {%- endif -%}
        <a href="{{ button_url }}" class="cta-banner__button">
          {{ section.settings.button_label }}
        </a>
      {%- endif -%}
    </div>
  </div>
</section>

<style>
  .cta-banner {
    background: var(--cta-bg, #1a1a1a);
    color: var(--cta-text, #ffffff);
  }
  .cta-banner__container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 1.5rem;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 3rem;
    align-items: center;
  }
  @media (max-width: 768px) {
    .cta-banner__container {
      grid-template-columns: 1fr;
      text-align: center;
    }
  }
  .cta-banner__media img {
    width: 100%;
    height: auto;
    border-radius: 8px;
  }
  .cta-banner__overline {
    font-size: 0.75rem;
    letter-spacing: 0.15em;
    text-transform: uppercase;
    opacity: 0.7;
    margin-bottom: 0.75rem;
  }
  .cta-banner__heading {
    font-size: clamp(1.75rem, 4vw, 2.5rem);
    line-height: 1.15;
    margin-bottom: 1rem;
  }
  .cta-banner__description {
    line-height: 1.7;
    opacity: 0.85;
    margin-bottom: 1.25rem;
  }
  .cta-banner__price {
    font-size: 1.25rem;
    font-weight: 700;
    margin-bottom: 1.5rem;
  }
  .cta-banner__button {
    display: inline-block;
    padding: 0.875rem 2.25rem;
    background: var(--cta-btn-bg, #ffffff);
    color: var(--cta-btn-text, #1a1a1a);
    text-decoration: none;
    font-weight: 600;
    border-radius: 4px;
    transition: opacity 0.2s;
  }
  .cta-banner__button:hover { opacity: 0.85; }
</style>

{% schema %}
{
  "name": "CTA Banner",
  "tag": "section",
  "settings": [
    {
      "type": "image_picker",
      "id": "image",
      "label": "Image"
    },
    {
      "type": "text",
      "id": "overline",
      "label": "Overline text",
      "default": "Limited Time"
    },
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Get 20% Off Your First Order"
    },
    {
      "type": "richtext",
      "id": "description",
      "label": "Description",
      "default": "<p>Join thousands of happy customers. Use code WELCOME20 at checkout.</p>"
    },
    {
      "type": "product",
      "id": "product",
      "label": "Featured product (optional)"
    },
    {
      "type": "text",
      "id": "button_label",
      "label": "Button label",
      "default": "Shop Now"
    },
    {
      "type": "url",
      "id": "button_url",
      "label": "Button link (overrides product link)"
    },
    {
      "type": "color",
      "id": "background_color",
      "label": "Background color",
      "default": "#1a1a1a"
    },
    {
      "type": "color",
      "id": "text_color",
      "label": "Text color",
      "default": "#ffffff"
    },
    {
      "type": "color",
      "id": "button_color",
      "label": "Button color",
      "default": "#ffffff"
    },
    {
      "type": "color",
      "id": "button_text_color",
      "label": "Button text color",
      "default": "#1a1a1a"
    },
    {
      "type": "range",
      "id": "padding_top",
      "min": 0,
      "max": 100,
      "step": 4,
      "unit": "px",
      "default": 60,
      "label": "Top padding"
    },
    {
      "type": "range",
      "id": "padding_bottom",
      "min": 0,
      "max": 100,
      "step": 4,
      "unit": "px",
      "default": 60,
      "label": "Bottom padding"
    }
  ],
  "presets": [
    {
      "name": "CTA Banner"
    }
  ]
}
{% endschema %}

The product setting type is the key here. When a merchant selects a product, the section automatically has access to its price, images, URL, and all metafields. If no product is selected, the section falls back to a generic CTA. This kind of conditional logic is trivial in Liquid and impossible to replicate cleanly in most page builder apps.

Dynamic Sources: Connecting Sections to Metafields

Holographic visualization connecting dynamic section data to metafields.

Shopify's dynamic sources feature takes native sections even further. Instead of hardcoding content or relying on section settings alone, you can connect any setting to a metafield — product metafields, variant metafields, or shop metafields.

How Dynamic Sources Work

In the theme editor, every text, image, and URL setting shows a small "Connect dynamic source" icon. Clicking it lets merchants bind that setting to a metafield. For example:

  • A testimonial section's quote text could pull from a product metafield containing a customer review.
  • A hero banner's image could pull from a collection's metafield containing a lifestyle photo.
  • A feature grid's description could pull from a shop metafield containing a shipping policy.

You don't need to change any code for this to work. If your section settings use standard types (text, image_picker, url, etc.), dynamic sources are automatically available.

Metafield-Aware Settings

For deeper metafield integration, you can read metafields directly in Liquid:

liquidliquid
{%- assign custom_badge = product.metafields.custom.badge_text -%}
{%- assign care_instructions = product.metafields.custom.care_instructions -%}
{%- assign size_chart_image = product.metafields.custom.size_chart -%}

{%- if custom_badge != blank -%}
  <span class="product-badge">{{ custom_badge }}</span>
{%- endif -%}

{%- if care_instructions != blank -%}
  <div class="care-instructions">
    {{ care_instructions | metafield_tag }}
  </div>
{%- endif -%}

{%- if size_chart_image != blank -%}
  <img src="{{ size_chart_image.value | image_url: width: 800 }}" alt="Size chart" loading="lazy">
{%- endif -%}

This pattern means merchants can create rich, product-specific content that renders inside your native sections — without any app involved. First Pier's guide to Shopify Liquid customization covers advanced metafield patterns including multi-line text fields and JSON metafields.

Dynamic sources combined with native sections create an editing experience that's more powerful than any page builder. The merchant controls the layout through section settings. The dynamic content comes from metafields. And everything renders as clean, server-side HTML.

Organizing Settings With Headers and Paragraphs

As your sections grow more complex, the theme editor sidebar can become overwhelming. Shopify provides two tools for organizing settings into a clear UI: header and paragraph settings.

jsonjson
"settings": [
  {
    "type": "header",
    "content": "Content"
  },
  {
    "type": "text",
    "id": "heading",
    "label": "Heading"
  },
  {
    "type": "textarea",
    "id": "description",
    "label": "Description"
  },
  {
    "type": "header",
    "content": "Layout"
  },
  {
    "type": "paragraph",
    "content": "Controls the visual layout of this section."
  },
  {
    "type": "select",
    "id": "layout",
    "label": "Layout",
    "options": [
      { "value": "contained", "label": "Contained" },
      { "value": "full-width", "label": "Full width" }
    ],
    "default": "contained"
  },
  {
    "type": "range",
    "id": "columns",
    "min": 1,
    "max": 4,
    "step": 1,
    "default": 3,
    "label": "Columns"
  },
  {
    "type": "header",
    "content": "Colors"
  },
  {
    "type": "color",
    "id": "background_color",
    "label": "Background"
  },
  {
    "type": "color",
    "id": "text_color",
    "label": "Text"
  },
  {
    "type": "header",
    "content": "Spacing"
  },
  {
    "type": "range",
    "id": "padding_top",
    "min": 0,
    "max": 100,
    "step": 4,
    "unit": "px",
    "default": 40,
    "label": "Top padding"
  },
  {
    "type": "range",
    "id": "padding_bottom",
    "min": 0,
    "max": 100,
    "step": 4,
    "unit": "px",
    "default": 40,
    "label": "Bottom padding"
  }
]

Group your settings consistently across all sections — Content, Layout, Colors, Spacing — and merchants will learn the pattern and feel confident editing any section in your theme. This is the kind of polish that separates a professional custom Shopify section library from a hacked-together one.

When to Keep a Page Builder — And How to Migrate Off One

Intellectual honesty matters. There are two scenarios where page builder apps remain a reasonable choice:

Non-technical merchants building one-off landing pages. If a merchant has no developer access, no budget for a freelancer, and needs a single promotional landing page next week, a page builder app gets the job done. It's a temporary solution, but it's fast.

Rapid prototyping before committing to code. Some agencies use page builders to mock up layouts with merchants, then rebuild the approved design as native sections. It's an expensive wireframing tool, but it short-circuits the revision cycle.

In every other case — stores with a developer (even part-time), stores that care about performance, stores building a section library for long-term use — native sections are the better choice. Instant.so's comparison of Shopify page builders provides context on when third-party tools fit specific workflows, but their own analysis acknowledges the performance costs.

The pages you build for high-converting Shopify landing pages will always perform better when they're built natively rather than through an app layer.

Migrating From a Page Builder to Native Sections

If you're currently using a page builder app and want to migrate, here's the process:

Step 1: Audit Your Existing Pages

List every page, section type, and content element built with the app. Common patterns:

  • Hero banners
  • Image + text rows
  • Testimonials/reviews
  • Feature/icon grids
  • FAQ accordions
  • Countdown timers
  • CTA banners
  • Custom product layouts

Step 2: Build Native Equivalents

For each section type in your audit, create a native .liquid section with the same settings. The four examples earlier in this article cover the most common patterns. Our complete guide to building custom Shopify sections covers additional patterns including hero banners, image-with-text, and collection grids.

Step 3: Recreate Content in the Theme Editor

Add your new native sections to each page via the theme editor, copy the content from the app-built versions, and configure settings. Do this page-by-page, verifying each one before moving on.

Step 4: Uninstall the App

Once every page is rebuilt with native sections:

  1. Verify all pages render correctly
  2. Check for leftover app code in your theme (some apps inject Liquid into theme.liquid)
  3. Uninstall the app from your Shopify admin
  4. Run a site audit to confirm no broken pages

The migration is tedious but the payoff is immediate: faster pages, lower costs, and zero vendor lock-in.

Build Your Section Library Today

Holographic visualization comparing native vs external app architectures.

Every section you build natively is a section you'll never pay a monthly fee for. The four examples in this article — testimonials, FAQ, feature grid, CTA banner — cover the sections that drive the most page builder app installations. Paste them into your theme, customize the settings, and you have a conversion-optimized store built on clean, fast, maintainable code.

The section schema system is the most underused feature in Shopify development. It gives merchants full visual control without sacrificing performance, and it gives developers a clean, predictable architecture that scales with the store. If you're spending $99/month on a page builder app, that money is better spent on a developer who can build native sections that will outlast any app.

Start with one section. Replace one app-built page with a native version. Measure the performance difference. Then do the next one. Within a few weeks, you'll have a section library that makes your store faster, cheaper to run, and entirely under your control.

Theme DesignShopify Development
Talk Shop

About Talk Shop

The Talk Shop team — insights from our community of Shopify developers, merchants, and experts.

Related Insights

Related

How to Create a Brand Identity for Your Shopify Store

Related

Shopify Schema Markup for SEO: The Complete Implementation Guide

The ecommerce newsletter that's actually useful.

Daily trends, teardowns, and tactics from the top 1% of ecommerce brands. Delivered every morning.

No spam. Unsubscribe anytime.

New

Business Name Generator

Generate unique, brandable business names with AI. Check domain availability instantly.

Generate Names

Talk Shop Daily

Daily ecommerce news, teardowns, and tactics.

No spam. Unsubscribe anytime.

Try our Business Name Generator