Why Custom Sections Matter
Most merchants use off-the-shelf themes and premade sections. They work fine until they don't—and suddenly you're either paying $200/month for an app or rewriting themes by hand.
Custom sections change the calculus. They let merchants (and their developers) ship features without forking the entire theme. A custom section is essentially a self-contained Liquid component with a schema that tells Shopify's editor how to render settings.
Here's the insider reality: most Shopify themes were built in 2018-2019 and haven't fundamentally changed. The section system, though, has matured massively. You can now build sections that would have required custom JavaScript five years ago—all with native Shopify controls. This means lower maintenance, fewer bugs, and less technical debt.
The Anatomy of a Shopify Section
A section has three parts:
- Liquid Template — renders the HTML and loops over block data
- JSON Schema — defines settings, blocks, and editor controls
- CSS/JavaScript — handles styling and interactivity
Here's a minimal example:
<div class="custom-hero">
<h1>{{ section.settings.heading }}</h1>
<p>{{ section.settings.description }}</p>
{% for block in section.blocks %}
{% if block.type == 'button' %}
<a href="{{ block.settings.link }}" class="btn">
{{ block.settings.label }}
</a>
{% endif %}
{% endfor %}
</div>
{% stylesheet %}
.custom-hero {
padding: 40px 20px;
text-align: center;
}
{% endstylesheet %}
{% schema %}
{
"name": "Custom Hero",
"settings": [
{
"type": "text",
"id": "heading",
"label": "Heading"
},
{
"type": "textarea",
"id": "description",
"label": "Description"
}
],
"blocks": [
{
"type": "button",
"name": "Button",
"settings": [
{
"type": "text",
"id": "label",
"label": "Button Label"
},
{
"type": "url",
"id": "link",
"label": "Button URL"
}
]
}
]
}
{% endschema %}
The JSON schema is what powers the theme editor UI. It tells Shopify: "this section has a heading and description setting, and it can contain repeatable buttons." That's all the editor needs to render drag-and-drop controls.
Schema Types That Actually Get Used
Shopify's full schema documentation lists 20+ input types. Here are the ones that matter:
| Type | Use Case | Notes |
|---|---|---|
| text | Short strings (titles, labels) | Max 255 chars |
| textarea | Long-form text | Use for descriptions, CTAs |
| richtext | HTML formatting | Renders editor with bold/italic/lists |
| number | Numeric inputs | Useful for spacing, durations |
| range | Slider controls | Better UX than text input for ranges |
| select | Dropdown choice | Limited set of predefined options |
| url | Link selector | Opens Shopify's link picker |
| color | Color picker | Returns hex value |
| image_picker | Image upload | Returns image object |
| collection, product | Shopify resource pickers | Returns handle or ID |
Pro tip: avoid radio buttons and checkboxes in 2026. They clutter the editor. Use select dropdowns or toggles instead.
Building Interactive Sections with JavaScript
The gotcha with sections is that they're static Liquid by default. If you need real interactivity—accordions, carousels, modal galleries—you need JavaScript.
Shopify sections support inline JavaScript via the {% javascript %} tag:
{% javascript %}
document.addEventListener('shopify:section:load', (event) => {
const section = event.detail.section;
const container = event.target.querySelector('.accordion-wrapper');
container.querySelectorAll('.accordion-header').forEach(header => {
header.addEventListener('click', () => {
const body = header.nextElementSibling;
body.classList.toggle('active');
});
});
});
document.addEventListener('shopify:section:unload', (event) => {
console.log('Section unloaded');
});
{% endjavascript %}
The shopify:section:load and shopify:section:unload events fire when a section is added/removed or when the theme editor updates it. This is critical for theme editor live preview.
Most developers make this mistake: they attach event listeners globally without cleanup. In the theme editor, users drag sections around—and if your JavaScript doesn't clean up, you'll get duplicate listeners and memory leaks.
Reusable Blocks for Complex Layouts
Blocks are how you let users add repeatable content without custom CSS. Say you're building a team page section—you probably want users to add multiple team members.
"blocks": [
{
"type": "team_member",
"name": "Team Member",
"settings": [
{
"type": "image_picker",
"id": "image",
"label": "Photo"
},
{
"type": "text",
"id": "name",
"label": "Name"
},
{
"type": "text",
"id": "title",
"label": "Title"
},
{
"type": "textarea",
"id": "bio",
"label": "Bio"
}
]
}
]
In your Liquid template:
<div class="team-grid">
{% for block in section.blocks %}
<div class="team-card">
<img src="{{ block.settings.image | image_url: width: 300 }}" alt="">
<h3>{{ block.settings.name }}</h3>
<p class="title">{{ block.settings.title }}</p>
<p>{{ block.settings.bio }}</p>
</div>
{% endfor %}
</div>
One non-obvious win: use forloop.index to create unique IDs for JavaScript hooks. This prevents collisions if the same section appears twice on a page:
<div id="accordion-{{ section.id }}-{{ forloop.index }}" class="accordion">
...
</div>
CSS Best Practices for Sections
Sections inherit the theme's base CSS. Don't assume anything. Always scope your styles:
{% stylesheet %}
.{{ section.id }}-custom-hero {
padding: {{ section.settings.padding_top | default: 40 }}px 20px;
background: {{ section.settings.bg_color }};
}
.{{ section.id }}-custom-hero h1 {
font-size: {{ section.settings.heading_size | default: 32 }}px;
color: {{ section.settings.heading_color }};
}
{% endstylesheet %}
Use the section ID as a CSS class prefix. This prevents style conflicts when the same section appears multiple times, or when you have multiple custom sections on one theme.
Also: CSS variables work in sections. Use them for theming:
{% stylesheet %}
:root {
--section-primary: {{ section.settings.primary_color }};
--section-spacing: {{ section.settings.spacing }}px;
}
.custom-section {
color: var(--section-primary);
margin: var(--section-spacing);
}
{% endstylesheet %}
Testing Your Section in the Theme Editor
You can't publish a section if it breaks the theme editor. Test these scenarios:
- Add section — does it load without errors?
- Change settings — does live preview update instantly?
- Add/remove blocks — any memory leaks or duplicate listeners?
- Duplicate section — does it get a new ID? Do styles conflict?
- Mobile preview — does it render correctly on small screens?
One anti-pattern: avoid Liquid conditions that depend on external APIs or metafield data. They'll slow down the theme editor. Load dynamic data server-side or via JavaScript after the section renders.
Integrating with Metafields for Advanced Use Cases
If you're building a section that pulls custom product data, use metafields. They let you store data that isn't part of the base product object.
For example, a custom product comparison section might pull competitor pricing from a metafield:
"settings": [
{
"type": "text",
"id": "metafield_namespace",
"label": "Metafield Namespace",
"default": "custom.competitors"
}
]
Then in Liquid:
{% assign competitor_data = product.metafields[section.settings.metafield_namespace] %}
This ties your section to the data layer without hardcoding values.
Common Pitfalls and How to Avoid Them
Pitfall 1: Over-engineering the schema. Don't expose 50 settings. Users get overwhelmed. Pick the 5-7 that actually matter: heading, background color, alignment, maybe button style. Hide the rest or use presets.
Pitfall 2: Assuming all images are the same ratio. Always use the image_url filter with explicit dimensions. Lazy-load images:
<img
src="{{ block.settings.image | image_url: width: 300 }}"
alt="{{ block.settings.alt_text }}"
loading="lazy"
>
Pitfall 3: Hardcoding breakpoints in CSS. Use CSS Grid and Flexbox instead of media queries. Modern CSS handles responsive design without breakpoints.
Pitfall 4: Not versioning your section. If you ship a breaking schema change, old sections break. Use version comments in your section name: "name": "Hero v2" when you make major changes.
Publishing Your Section
To add a section to your theme:
- Connect to your theme via the theme code editor (Shopify admin > Sales channels > Online Store > Customize theme > Edit code)
- Create a new section file:
sections/custom-hero.liquid - Paste your code
- Save
It's now available in the theme editor. Merchants can add it to any page.
If you're building for a client, save your sections to version control (GitHub) so changes are tracked. Use a branch for development, merge to main only after testing.
Key Takeaway
Custom sections are the fastest way to add unique functionality without hiring a developer or buying another app. They're faster to build than you think—most sections are 60-80 lines of Liquid—and they significantly reduce theme maintenance burden. Start small, test in the editor, and iterate.
The difference between a 2-day custom section and a 2-month custom app is the ability to ship incrementally. Sections let you do that.
FAQ
Q: Can I sell my custom section on the Shopify App Store? A: No. Sections are theme-only. To distribute functionality across themes, build an app instead.
Q: What's the performance cost of custom sections? A: Minimal. Sections are compiled Liquid—the same as theme code. The real cost is theme editor latency if your Liquid loops over thousands of items.
Q: Can sections have their own JavaScript bundles? A: Not natively. You can use the {% javascript %} tag for inline scripts, or load external scripts via the section's URL. For complex bundles, consider Shopify's Asset Loader API.
Q: How do I handle translations in custom sections?
A: Use Shopify's translation system. Create a locales/en.json file and use the t filter in Liquid: {{ section.settings.heading | t }}.
Q: Can I make sections conditional on store plan? A: Not directly. Shopify doesn't expose plan info to sections. Use apps or custom code to conditionally hide sections for specific plans.
Ready to build your first section? Start with a hero section—it's simple enough to learn the basics, but complex enough to be useful. Then move to more advanced patterns like carousels or galleries.
For a live example of modern custom sections, check out how Shopify's own themes (Dawn, Prestige) build sections. Their source is public on GitHub.
Resources & Further Reading
- Shopify Theme Development Docs: https://shopify.dev/docs/themes
- Liquid Reference: https://shopify.dev/docs/themes/liquid/reference
- Section Schema Reference: https://shopify.dev/docs/themes/architecture/sections/section-schema
Ready to optimize your Shopify store or build custom functionality? Get in touch with Tenten — we specialize in custom Shopify development and theme optimization.