Skip to content
Ecom

Shopify Section Schema: Patterns and Anti-Patterns

A reference-style breakdown of Shopify section schema. The eight presets we use on ninety percent of builds and the anti-patterns that make merchants curse your name.

By WitsCode10 min read

After building on more than two hundred and fifty Shopify stores, the single biggest predictor of whether a merchant will love their theme a year from launch is not the design, the animations, or the pagespeed score. It is the section schema. Schema is the contract between the developer and the merchant. Write it well and the merchant ships campaigns in minutes. Write it badly and they will be emailing you every Friday asking why the homepage broke again. This is a reference post on how we approach section schema at WitsCode, the eight presets we reuse on nearly every build, and the anti-patterns that keep showing up on audits.

What section schema actually is

Every Shopify section file ends with a {% schema %} block containing a JSON object. That JSON is parsed by the theme editor and turned into the sidebar UI a merchant uses to configure the section. The top-level keys matter more than most tutorials admit. name is the label in the Add section picker. tag controls the wrapping HTML element and defaults to div, but you should be reaching for section, header, footer, article, or aside where semantics warrant, both for accessibility and for screen readers that rely on landmark roles. class adds a CSS class on that wrapper. limit caps how many instances of the section can be added to one template, which matters for sections like a cart drawer trigger that should appear exactly once. settings is the array of controls. blocks defines repeatable sub-units inside the section, each with its own settings. max_blocks caps the total blocks and defaults to sixteen with an upper ceiling of fifty. presets is what makes a section addable from the editor picker. enabled_on and disabled_on gate where the section is allowed to live. Skipping any of these is the usual entry point to the mess.

The eight presets we use on ninety percent of builds

If you audit a well-built Shopify 2.0 theme and count the truly distinct sections, you rarely find more than eight. Our WitsCode section library converges on hero, featured collection, rich text, image with text, multi-column, testimonials, featured product, and newsletter. Everything else a merchant thinks they need is a variation on those eight, expressed through settings and blocks rather than new section files. A hero with a video background is the same section as a hero with a still image, toggled by a video_url setting with a visible_if condition. A three-column layout and a four-column layout of feature callouts are the same multi-column section with a range setting for columns per row. This matters because every time you ship a new preset you are asking the merchant to learn a new mental model. Eight presets fit on one screen. Twenty-seven do not.

Settings types and when to reach for each

The full setting input list is longer than most developers remember. The basics are text, textarea, number, range, checkbox, radio, and select. The specialized inputs are where the power sits: color_scheme and color_scheme_group for theme-wide token binding, image_picker with video and video_url for media, richtext and inline_richtext for merchant-friendly formatting, url, link_list, collection, collection_list, product, product_list, blog, page, article, metaobject, and metaobject_list for resource pickers, and the newer style.layout_panel, style.size_panel, and style.spacing_panel for layout controls that match Shopify's own visual editor. Avoid html unless you truly need raw markup, because it bypasses sanitization and turns into a content security policy problem the moment a merchant pastes a script tag from their analytics vendor. Reach for richtext instead.

A setting entry looks like {"type": "text", "id": "heading", "label": "t:sections.hero.settings.heading.label", "default": "Welcome to the store"}. Notice the t: prefix on the label. That is a reference into locales/en.default.schema.json and its sibling locale files, and it is the only correct way to write labels if you plan to ship to a merchant who might ever add a second language. Hardcoded English strings work today and break the day Shopify Markets gets turned on.

Conditional settings with visible_if

One of the most underused features is visible_if. Write "visible_if": "{{ section.settings.show_button }}" on the button label and button link settings and they disappear from the sidebar until the merchant toggles show_button on. This is the single best tool for keeping setting counts visually low without sacrificing flexibility. A section with thirty settings that hides twenty of them behind conditionals feels like a section with ten settings, and merchants stop scrolling looking for the thing they want.

Blocks and max_blocks

Blocks are the repeatable units inside a section. A testimonials section has a quote block with settings for quote text, author name, and avatar image. A multi-column section has a column block. Each block entry looks like {"type": "quote", "name": "Quote", "limit": 20, "settings": [...]}. The section-level max_blocks caps the total across all block types. If you leave max_blocks at the default, a merchant can happily add fifty blocks into a hero carousel and wonder why their Largest Contentful Paint cratered. Pick a realistic number. Four for hero slides, eight for featured products, twelve for testimonials. Constraints are a feature, not a limitation.

Presets versus default

This trips up almost every junior developer. A section rendered statically from theme.liquid or a section group uses default to describe its initial configuration. A section meant to be added dynamically by merchants uses presets. Mixing them or adding both to the same schema produces theme editor errors that are nearly impossible to search for because the error messages are generic. The rule is simple. If the section ever appears in the Add section picker, it needs presets. If it only exists because {% section 'announcement-bar' %} is written in a layout file, it needs default. Never both.

A preset entry looks like {"name": "Featured collection", "settings": {"heading": "Featured products", "collection": ""}, "blocks": [{"type": "product_card"}, {"type": "product_card"}, {"type": "product_card"}, {"type": "product_card"}]}. Notice the default heading and the four block instances. Merchants who drop this section get a section that already looks populated. That is the goal. A preset with empty strings and zero blocks lands on the page as an empty grey box and the merchant immediately thinks the developer shipped something broken.

Preset sprawl, the anti-pattern nobody admits to

Preset sprawl is what happens when a developer, under pressure from a client who saw a competitor with fancy layouts, ships eight variations of the same section as separate presets. Featured collection two columns. Featured collection three columns. Featured collection four columns. Featured collection carousel. Featured collection with banner. Featured collection grid. Featured collection masonry. Featured collection tabbed. The merchant opens the Add section picker and sees a wall of nearly identical names. They pick wrong. They delete it. They try the next one. They pick wrong again. They file a support ticket.

The fix is always the same. One preset, one section file, with a layout setting that toggles columns, a style setting that toggles grid versus carousel, and visible_if conditions that only reveal carousel-specific settings when carousel is selected. You end up with one entry in the picker, half the code to maintain, and a merchant who learns the section once and reuses the knowledge every time.

enabled_on and disabled_on template gating

A product description tabs section belongs on product pages. A collection filter sidebar belongs on collection pages. A header belongs in the header section group. And yet, on nearly every audit, we find sections that can be dropped onto any template with predictable consequences. Merchants put headers on product pages, cart sections on blog articles, and announcement bars inside the footer. The schema has had a clean fix for years and it is called enabled_on.

Write "enabled_on": {"templates": ["product"]} on a product-only section and it vanishes from every other template's picker. Write "enabled_on": {"groups": ["header"]} on a navigation section and it only appears inside section groups tagged header. You can also go the other way with "disabled_on": {"templates": ["cart", "checkout"]} if allowlisting is easier to reason about than blocklisting. The effect on merchant experience is immediate. They stop making mistakes because the schema stops letting them.

Section groups and where the header lives

Section groups changed the game when Shopify introduced them. A section group is a JSON file in the sections folder that groups multiple sections and allows merchants to reorder them in the theme editor. The header group typically contains an announcement bar, a main header, and sometimes a secondary nav. You render it in the layout with {% sections 'header-group' %} rather than the old {% section 'header' %}. This unlocks merchant control over announcement bars and promotional strips above the header without touching code. If your theme still has hardcoded announcement bars in theme.liquid, you are leaving merchant autonomy on the table.

Dynamic sources and the metaobject binding

The feature that separates good schemas from great ones is dynamic source support. Most setting types can be bound, in the theme editor, to a metafield or metaobject field via the dynamic source picker. The merchant clicks the small database icon next to the setting, picks a metafield namespace and key, and the setting is now populated from structured content instead of typed text. This is the trick that lets a content team edit a landing page module from a metaobject entry and see the changes reflected anywhere the module renders.

For metaobject-specific inputs, the schema entry is {"type": "metaobject", "id": "testimonial_ref", "label": "Testimonial", "metaobject_type": "testimonial"} where testimonial is the handle of a metaobject definition in the Shopify admin. Pair this with metaobject_list for repeaters and you have a fully content-modeled section without writing a single line of backend code. Merchants love it because they manage content in one place. Content teams love it because structured content is reusable. Developers love it because there is no bespoke schema to maintain per landing page.

Translation keys, the setting label pattern

Every label, info string, and default text that a merchant will read should use a translation key. The convention is t:sections.<section>.settings.<id>.label with a matching entry in locales/en.default.schema.json. Do this from day one. Retrofitting translation keys onto a theme with sixty hardcoded English labels across twenty sections is an afternoon you will not get back. The payoff is that your theme ships to German, French, and Japanese merchants without any schema edits. Shopify handles the lookup and fallback automatically.

Defaults for text content also support translation keys. "default": "t:sections.hero.settings.heading.default" pulls the default heading from the locale file. This is how you keep placeholder copy consistent across locales and avoid the awkward moment when a French merchant drops in a hero preset and sees Welcome to the store in English.

The forty settings anti-pattern

The single most common cause of merchant frustration we see on audits is setting bloat. A section ships with thirty, forty, sometimes fifty settings, most of which the merchant will never touch. Padding top, padding bottom, padding left, padding right, mobile padding top, mobile padding bottom, background color, background color mobile, text color, text color mobile, heading size, heading size mobile, heading size tablet, and so on. Each one is individually defensible. Collectively they are unusable.

The discipline is to ask, for every setting, whether a merchant will realistically change it in the next six months. If not, kill it or move it to a style preset. Use color_scheme instead of individual color pickers and bind to the theme's scheme system so merchants change colors globally. Use style.spacing_panel for padding controls that match Shopify's native spacing UI rather than four separate number inputs. Use visible_if to hide advanced controls behind a toggle. Aim for fewer than fifteen visible settings at any time. If the section needs more than that, it is probably two sections pretending to be one.

Quick schema hygiene checklist

Before shipping any section, read the schema and check five things. One, every label and info string uses a t: translation key. Two, every dynamic-addable section has presets with populated defaults and at least one block if blocks exist. Three, enabled_on or disabled_on restricts the section to templates where it makes sense. Four, max_blocks is set to a realistic number. Five, tag is set to a semantic element where appropriate. If all five are green, you have shipped a schema the merchant will still like in a year.

Where this sits in a real build

Clean section schema is not a nice-to-have. It is the line between a theme the merchant owns and a theme the merchant rents from the agency. At WitsCode we ship every build with our section library of eight productized presets, every setting using translation keys, every section gated with enabled_on, every block capped, and every merchant-editable content stream bound to metaobjects. The result is a store the client can run without us, which is exactly the goal. If your current theme is a forty-setting monster with twenty-seven preset variations and a support ticket queue, the fix is a rebuild on a lean schema, not another round of patches. -> Book a WitsCode section library build or custom schema audit and we will send you the before-and-after on the last store we shipped.

Get weekly field notes.

Practical writing on shipping products, straight to your inbox. No spam.

Need help with this?

Shopify Development

We design and build web apps, MVPs, and SaaS products. Talk to us about what you are working on.

Talk to us

Want to discuss ecom for your business?

Start a project and we'll talk through where you are, what's working, and the highest-leverage moves for the next 90 days.