A static feature grid usually repeats the same card markup three or four times. Copying that markup into a Shopify section works visually, but it leaves the merchant unable to add, remove, or reorder cards. A block is the right model when one visual pattern represents a variable-length list of content.
This tutorial converts a three-card HTML grid into one complete section. The result supports up to six cards, preserves each card's theme-editor selection state, renders responsive images, and stays useful when a merchant leaves an optional field empty.
Prerequisites
You need access to a Shopify development theme and basic familiarity with the sections/ directory. Shopify CLI is helpful for local previewing, but the same file can be added in the Shopify code editor. Review the section schema reference if blocks and presets are new to you.
The repeated HTML input
Here is the original static markup. The repeated .feature-card element is the signal that a block may be a better fit than three groups of numbered section settings.
<section class="feature-grid">
<h2>Why shoppers choose this collection</h2>
<div class="feature-grid__items">
<article class="feature-card">
<img src="materials.jpg" alt="Natural fabric swatches">
<h3>Considered materials</h3>
<p>Natural fibres selected for everyday comfort.</p>
</article>
<article class="feature-card">
<img src="packing.jpg" alt="A recyclable shipping box">
<h3>Lower-waste packing</h3>
<p>Orders ship in recyclable paper packaging.</p>
</article>
<article class="feature-card">
<img src="care.jpg" alt="A garment care label">
<h3>Useful care guidance</h3>
<p>Clear instructions help products last longer.</p>
</article>
</div>
</section>
Each card has the same content model: image, heading, rich text, and an optional link. That model becomes one block type. Shopify repeats the Liquid inside for block in section.blocks for every configured block.
Complete Liquid section
Create sections/feature-grid.liquid and paste this complete file:
{% style %}
#shopify-section-{{ section.id }} .feature-grid {
padding-block: 3rem;
}
#shopify-section-{{ section.id }} .feature-grid__heading {
margin: 0 0 1.5rem;
}
#shopify-section-{{ section.id }} .feature-grid__items {
display: grid;
grid-template-columns: 1fr;
gap: 1.25rem;
}
#shopify-section-{{ section.id }} .feature-card__image {
display: block;
width: 100%;
height: auto;
aspect-ratio: 4 / 3;
object-fit: cover;
}
@media (min-width: 750px) {
#shopify-section-{{ section.id }} .feature-grid__items {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
{% endstyle %}
<section
class="feature-grid page-width"
{% if section.settings.heading != blank %}aria-labelledby="FeatureGridHeading-{{ section.id }}"{% endif %}
>
{% if section.settings.heading != blank %}
<h2 id="FeatureGridHeading-{{ section.id }}" class="feature-grid__heading">
{{ section.settings.heading | escape }}
</h2>
{% endif %}
<div class="feature-grid__items">
{% for block in section.blocks %}
<article class="feature-card" {{ block.shopify_attributes }}>
{% if block.settings.image != blank %}
{{
block.settings.image
| image_url: width: 900
| image_tag:
class: 'feature-card__image',
widths: '320, 480, 640, 900',
sizes: '(min-width: 750px) 33vw, 100vw',
loading: 'lazy',
alt: block.settings.image.alt
}}
{% endif %}
{% if block.settings.heading != blank %}
<h3>{{ block.settings.heading | escape }}</h3>
{% endif %}
{% if block.settings.body != blank %}
<div class="rte">{{ block.settings.body }}</div>
{% endif %}
{% if block.settings.link_label != blank and block.settings.link != blank %}
<a href="{{ block.settings.link }}">
{{ block.settings.link_label | escape }}
</a>
{% endif %}
</article>
{% else %}
<p>Add a feature block in the theme editor.</p>
{% endfor %}
</div>
</section>
{% schema %}
{
"name": "Feature grid",
"tag": "section",
"class": "section-feature-grid",
"max_blocks": 6,
"settings": [
{
"type": "text",
"id": "heading",
"label": "Heading",
"default": "Why shoppers choose this collection"
}
],
"blocks": [
{
"type": "feature",
"name": "Feature",
"settings": [
{
"type": "image_picker",
"id": "image",
"label": "Image"
},
{
"type": "text",
"id": "heading",
"label": "Heading",
"default": "Feature heading"
},
{
"type": "richtext",
"id": "body",
"label": "Text",
"default": "<p>Explain this feature in one or two sentences.</p>"
},
{
"type": "text",
"id": "link_label",
"label": "Link label"
},
{
"type": "url",
"id": "link",
"label": "Link"
}
]
}
],
"presets": [
{
"name": "Feature grid",
"blocks": [
{ "type": "feature" },
{ "type": "feature" },
{ "type": "feature" }
]
}
]
}
{% endschema %}
Why this implementation works
section.blocks follows the merchant's current order, so drag-and-drop reordering in the theme editor is reflected on the storefront. block.shopify_attributes is also important: Shopify uses those attributes to select and identify the block inside the editor preview. Do not put them on an unrelated parent outside the loop.
The block type is a stable identifier, while name is the merchant-facing label. Within one section, every setting ID must be unique inside its own settings array. IDs such as image, heading, and body can be reused by other block types because they live in different block scopes.
The rich-text value is rendered inside a div, not another paragraph, because a richtext setting can already contain <p> elements. The image uses image_url and image_tag so Shopify can generate candidates for the browser instead of sending one oversized asset to every viewport. See Shopify's block architecture documentation for the wider block model.
Expected output and responsive behaviour
With the preset untouched, the editor creates three feature blocks. They stack in one column below 750 pixels and form three equal columns on wider screens. Adding a fourth block creates another grid item; the browser wraps it automatically. Removing every block shows a short editor prompt instead of an empty, confusing section.
Image alternative text comes from the image chosen in Shopify's file picker. Ask the content editor to write alt text that describes the image's purpose; filenames are not useful alternatives. If the cards are only decorative, adjust the implementation deliberately rather than silently duplicating the heading as alt text.
Common failure cases
- Every card has numbered settings:
heading_1,heading_2, andheading_3set a hard limit and cannot be reordered naturally. Use blocks for repeatable content. - Blocks cannot be selected in the editor: confirm the repeated root element includes
{{ block.shopify_attributes }}. - The section is absent from “Add section”: a preset is required for a merchant-addable section.
- Schema fails to save: JSON cannot contain Liquid, comments, or trailing commas. Validate only the content between the schema tags as JSON.
- Links render with no label: guard both the URL and its visible label, as the example does.
- Images are blurry: the largest
image_urlwidth must be large enough for the card's rendered size and high-density screens.
Test in a development theme
Run shopify theme dev --store your-development-store from your theme directory, add Feature grid in the editor, and then verify these cases:
- Add, remove, duplicate, and reorder blocks.
- Leave each optional field empty in turn.
- Select images with and without saved alt text.
- Resize from a narrow phone viewport to a wide desktop viewport.
- Tab through every rendered link and confirm a visible focus style comes from the theme.
- Check the browser console and the theme editor for Liquid or schema errors.
Shopify documents the development-theme workflow in the Shopify CLI theme guide. Theme Check can catch additional Liquid and theme problems before a pull request.
Manual verification checklist
- Repeated items are blocks, not numbered settings.
- Every setting ID is unique in its scope.
- The schema parses as strict JSON.
- The preset references an existing block type.
-
block.shopify_attributesis present on each repeated item. - Empty fields do not produce empty links or headings.
- Images have responsive candidates and meaningful alt text.
- Reordering works in the theme editor and on the storefront.
You can use the HTML converter for a first-pass section and the schema builder to inspect setting definitions. The conversion still needs a developer to decide whether repeated markup should become blocks. For the broader workflow, continue with How to convert HTML into a Shopify section.
Conclusion
The useful conversion is not merely replacing three strings with Liquid variables. It is choosing a content model that matches how a merchant will maintain the section. A block loop keeps repeated content flexible while the schema, fallback states, responsive image output, and editor attributes make that flexibility reliable.