Collection feature grid
section.
This collection feature grid drops onto any Shopify homepage, landing page, or category index. You add a collection per block in the theme editor, optionally override the image and title, and choose between three card layouts: image above text, text over image, or image beside text on desktop. Columns, gap, aspect ratio, and product-count display are all configurable. It works without any third-party app.
The code
One file. Paste the whole thing into a new section.
{% comment %}
Jelonyx · Collection Feature Grid Section
Compatible with Dawn 12+, Horizon, and most OS 2.0 themes
Source: jelonyx.com/shopify/sections/collection-grid
{% endcomment %}
{%- liquid
assign cols_d = section.settings.columns_desktop | default: 3
assign cols_m = section.settings.columns_mobile | default: 1
assign card_style = section.settings.card_style | default: 'image_above'
assign ratio = section.settings.aspect_ratio | default: 'square'
assign gap = section.settings.gap | default: 20
assign pad_t = section.settings.padding_top | default: 60
assign pad_b = section.settings.padding_bottom | default: 60
-%}
{%- if section.blocks.size > 0 -%}
<section
id="jlx-cg-{{ section.id }}"
class="jlx-cg jlx-cg--{{ card_style }} jlx-cg--ratio-{{ ratio }}"
style="--jlx-cg-cols-d: {{ cols_d }}; --jlx-cg-cols-m: {{ cols_m }}; --jlx-cg-gap: {{ gap }}px; --jlx-cg-pt: {{ pad_t }}px; --jlx-cg-pb: {{ pad_b }}px;"
>
<div class="jlx-cg__inner">
{%- if section.settings.heading != blank -%}
<h2 class="jlx-cg__heading">{{ section.settings.heading }}</h2>
{%- endif -%}
{%- if section.settings.subheading != blank -%}
<p class="jlx-cg__subheading">{{ section.settings.subheading }}</p>
{%- endif -%}
<div class="jlx-cg__grid">
{%- for block in section.blocks -%}
{%- assign coll = block.settings.collection -%}
{%- if coll != blank -%}
{%- if block.settings.image_override != blank -%}
{%- assign card_image = block.settings.image_override -%}
{%- elsif coll.image != blank -%}
{%- assign card_image = coll.image -%}
{%- elsif coll.products.first.featured_image != blank -%}
{%- assign card_image = coll.products.first.featured_image -%}
{%- else -%}
{%- assign card_image = blank -%}
{%- endif -%}
{%- if block.settings.title_override != blank -%}
{%- assign card_title = block.settings.title_override -%}
{%- else -%}
{%- assign card_title = coll.title -%}
{%- endif -%}
<a
href="{{ coll.url }}"
class="jlx-cg__card"
{{ block.shopify_attributes }}
>
<div class="jlx-cg__media">
{%- if card_image != blank -%}
<img
src="{{ card_image | image_url: width: 800 }}"
srcset="{{ card_image | image_url: width: 400 }} 400w, {{ card_image | image_url: width: 600 }} 600w, {{ card_image | image_url: width: 800 }} 800w, {{ card_image | image_url: width: 1200 }} 1200w"
sizes="(min-width: 990px) calc(100vw / {{ cols_d }}), (min-width: 750px) 50vw, calc(100vw / {{ cols_m }})"
width="{{ card_image.width }}"
height="{{ card_image.height }}"
alt="{{ card_image.alt | default: card_title | escape }}"
loading="lazy"
class="jlx-cg__img"
/>
{%- else -%}
<div class="jlx-cg__img jlx-cg__img--empty" aria-hidden="true"></div>
{%- endif -%}
{%- if card_style == 'image_overlay' -%}
<div class="jlx-cg__overlay" aria-hidden="true"></div>
{%- endif -%}
</div>
<div class="jlx-cg__body">
<p class="jlx-cg__title">{{ card_title }}</p>
{%- if section.settings.show_subtitle and block.settings.subtitle != blank -%}
<p class="jlx-cg__subtitle">{{ block.settings.subtitle }}</p>
{%- endif -%}
{%- if section.settings.show_count -%}
<p class="jlx-cg__count">{{ coll.products_count }} products</p>
{%- endif -%}
{%- if block.settings.cta_label != blank -%}
<span class="jlx-cg__cta">{{ block.settings.cta_label }} <span aria-hidden="true">→</span></span>
{%- endif -%}
</div>
</a>
{%- endif -%}
{%- endfor -%}
</div>
</div>
</section>
<style>
#jlx-cg-{{ section.id }} {
padding: var(--jlx-cg-pt, 60px) 20px var(--jlx-cg-pb, 60px);
background: var(--color-background, #fff);
}
#jlx-cg-{{ section.id }} .jlx-cg__inner {
max-width: 1280px;
margin: 0 auto;
}
#jlx-cg-{{ section.id }} .jlx-cg__heading {
font-size: clamp(22px, 3.5vw, 34px);
font-weight: 700;
line-height: 1.2;
color: var(--color-foreground, #1a1a1a);
margin: 0 0 10px;
text-align: center;
}
#jlx-cg-{{ section.id }} .jlx-cg__subheading {
font-size: 15px;
line-height: 1.6;
color: var(--color-foreground-secondary, #6b6b6b);
text-align: center;
margin: 0 0 36px;
}
#jlx-cg-{{ section.id }} .jlx-cg__grid {
display: grid;
grid-template-columns: repeat(var(--jlx-cg-cols-m, 1), 1fr);
gap: var(--jlx-cg-gap, 20px);
}
@media (min-width: 750px) {
#jlx-cg-{{ section.id }} .jlx-cg__grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 990px) {
#jlx-cg-{{ section.id }} .jlx-cg__grid {
grid-template-columns: repeat(var(--jlx-cg-cols-d, 3), 1fr);
}
}
#jlx-cg-{{ section.id }} .jlx-cg__card {
display: block;
color: var(--color-foreground, #1a1a1a);
text-decoration: none;
border: 1px solid transparent;
border-radius: 10px;
overflow: hidden;
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
position: relative;
}
#jlx-cg-{{ section.id }} .jlx-cg__card:hover {
transform: translateY(-2px);
border-color: var(--color-border, rgba(0,0,0,0.12));
box-shadow: 0 8px 24px rgba(0,0,0,0.06);
}
#jlx-cg-{{ section.id }} .jlx-cg__media {
position: relative;
overflow: hidden;
background: var(--color-background-2, rgba(0,0,0,0.03));
}
#jlx-cg-{{ section.id }} .jlx-cg__img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.4s ease;
}
#jlx-cg-{{ section.id }} .jlx-cg__img--empty {
width: 100%;
aspect-ratio: 1 / 1;
background: var(--color-background-2, rgba(0,0,0,0.05));
}
#jlx-cg-{{ section.id }} .jlx-cg__card:hover .jlx-cg__img {
transform: scale(1.04);
}
#jlx-cg-{{ section.id }}.jlx-cg--ratio-square .jlx-cg__media { aspect-ratio: 1 / 1; }
#jlx-cg-{{ section.id }}.jlx-cg--ratio-portrait .jlx-cg__media { aspect-ratio: 3 / 4; }
#jlx-cg-{{ section.id }}.jlx-cg--ratio-landscape .jlx-cg__media { aspect-ratio: 4 / 3; }
#jlx-cg-{{ section.id }}.jlx-cg--ratio-auto .jlx-cg__media { aspect-ratio: auto; }
#jlx-cg-{{ section.id }} .jlx-cg__body {
padding: 14px 4px 4px;
}
#jlx-cg-{{ section.id }} .jlx-cg__title {
font-size: 16px;
font-weight: 600;
line-height: 1.3;
margin: 0;
color: var(--color-foreground, #1a1a1a);
}
#jlx-cg-{{ section.id }} .jlx-cg__subtitle {
font-size: 13px;
line-height: 1.5;
margin: 4px 0 0;
color: var(--color-foreground-secondary, #6b6b6b);
}
#jlx-cg-{{ section.id }} .jlx-cg__count {
font-size: 12px;
font-weight: 500;
margin: 6px 0 0;
color: var(--color-foreground-secondary, #888);
letter-spacing: 0.02em;
}
#jlx-cg-{{ section.id }} .jlx-cg__cta {
display: inline-block;
margin-top: 10px;
font-size: 13px;
font-weight: 600;
color: var(--color-foreground, #1a1a1a);
border-bottom: 1px solid currentColor;
padding-bottom: 1px;
}
/* image_overlay variant */
#jlx-cg-{{ section.id }}.jlx-cg--image_overlay .jlx-cg__card {
border: none;
}
#jlx-cg-{{ section.id }}.jlx-cg--image_overlay .jlx-cg__media {
border-radius: 10px;
}
#jlx-cg-{{ section.id }}.jlx-cg--image_overlay .jlx-cg__overlay {
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(0,0,0,0) 40%, rgba(0,0,0,0.55) 100%);
pointer-events: none;
}
#jlx-cg-{{ section.id }}.jlx-cg--image_overlay .jlx-cg__body {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 18px;
z-index: 2;
color: #fff;
}
#jlx-cg-{{ section.id }}.jlx-cg--image_overlay .jlx-cg__title,
#jlx-cg-{{ section.id }}.jlx-cg--image_overlay .jlx-cg__subtitle,
#jlx-cg-{{ section.id }}.jlx-cg--image_overlay .jlx-cg__count,
#jlx-cg-{{ section.id }}.jlx-cg--image_overlay .jlx-cg__cta {
color: #fff;
}
/* image_left variant */
@media (min-width: 750px) {
#jlx-cg-{{ section.id }}.jlx-cg--image_left .jlx-cg__card {
display: grid;
grid-template-columns: 40% 1fr;
align-items: center;
}
#jlx-cg-{{ section.id }}.jlx-cg--image_left .jlx-cg__body {
padding: 18px 18px 18px 20px;
}
}
</style>
{%- else -%}
{%- if request.design_mode -%}
<div style="padding:60px 20px;text-align:center;border:2px dashed rgba(0,0,0,0.12);border-radius:8px;">
<p style="color:#888;font-size:14px;margin:0;">Add collection blocks using the section settings to build your collection grid.</p>
</div>
{%- endif -%}
{%- endif -%}
{% schema %}
{
"name": "Collection feature grid",
"tag": "section",
"class": "section",
"max_blocks": 12,
"settings": [
{
"type": "text",
"id": "heading",
"label": "Heading",
"placeholder": "Shop by category"
},
{
"type": "text",
"id": "subheading",
"label": "Subheading"
},
{ "type": "header", "content": "Layout" },
{
"type": "select",
"id": "columns_desktop",
"label": "Columns on desktop",
"options": [
{ "value": "2", "label": "2" },
{ "value": "3", "label": "3" },
{ "value": "4", "label": "4" },
{ "value": "5", "label": "5" }
],
"default": "3"
},
{
"type": "select",
"id": "columns_mobile",
"label": "Columns on mobile",
"options": [
{ "value": "1", "label": "1" },
{ "value": "2", "label": "2" }
],
"default": "1"
},
{
"type": "select",
"id": "card_style",
"label": "Card style",
"options": [
{ "value": "image_above", "label": "Image above text" },
{ "value": "image_overlay", "label": "Text over image" },
{ "value": "image_left", "label": "Image beside text" }
],
"default": "image_above"
},
{
"type": "select",
"id": "aspect_ratio",
"label": "Image aspect ratio",
"options": [
{ "value": "square", "label": "Square (1:1)" },
{ "value": "portrait", "label": "Portrait (3:4)" },
{ "value": "landscape", "label": "Landscape (4:3)" },
{ "value": "auto", "label": "Original" }
],
"default": "square"
},
{
"type": "range",
"id": "gap",
"label": "Gap between cards",
"min": 0,
"max": 60,
"step": 4,
"unit": "px",
"default": 20
},
{ "type": "header", "content": "Card content" },
{
"type": "checkbox",
"id": "show_count",
"label": "Show product count",
"default": true
},
{
"type": "checkbox",
"id": "show_subtitle",
"label": "Show subtitle",
"default": true
},
{ "type": "header", "content": "Section padding" },
{
"type": "range",
"id": "padding_top",
"label": "Padding top",
"min": 0,
"max": 160,
"step": 4,
"unit": "px",
"default": 60
},
{
"type": "range",
"id": "padding_bottom",
"label": "Padding bottom",
"min": 0,
"max": 160,
"step": 4,
"unit": "px",
"default": 60
}
],
"blocks": [
{
"type": "card",
"name": "Collection card",
"settings": [
{
"type": "collection",
"id": "collection",
"label": "Collection"
},
{
"type": "image_picker",
"id": "image_override",
"label": "Image override",
"info": "Optional. Falls back to the collection image, then the first product image."
},
{
"type": "text",
"id": "title_override",
"label": "Title override",
"info": "Optional. Falls back to the collection title."
},
{
"type": "text",
"id": "subtitle",
"label": "Subtitle"
},
{
"type": "text",
"id": "cta_label",
"label": "CTA label",
"placeholder": "Shop now"
}
]
}
],
"presets": [
{
"name": "Collection feature grid",
"category": "Collection",
"blocks": [
{ "type": "card" },
{ "type": "card" },
{ "type": "card" }
]
}
]
}
{% endschema %}How to add the section
- 01Open your theme code editor.
In Shopify Admin, go to Online Store → Themes. On your active theme, click the three-dot menu and select Edit code.
- 02Create a new section file.
Under the Sections folder, click Add a new section. Name it
jlx-collection-gridand click Done. - 03Paste the full section code.
Delete any placeholder content in the new file, paste the entire code block above, and save.
- 04Add the section to a template.
Open the theme editor (Customize). Navigate to the page template where you want the grid. Click Add section and select Collection feature grid.
- 05Add collection cards as blocks.
Inside the section panel, click Add block for each card (up to 12). Pick the collection from the picker. Optionally override the image, title, subtitle, or CTA label per card. Save.
- 06Tune layout and behaviour.
In the section settings, choose columns_desktop (2-5), columns_mobile (1 or 2), the card style, and the image aspect ratio. Toggle product count and subtitle visibility. Adjust gap and padding to match your theme's rhythm.
Schema settings
Section-level settings (apply to the whole grid):
headingtextOptional heading above the grid.subheadingtextOptional subheading below the heading.columns_desktopselectColumns on screens ≥ 990px. Choose from 2, 3, 4, or 5. Default 3.columns_mobileselectColumns on screens < 750px. 1 or 2. Default 1.card_styleselectimage_above (default: image on top, text below), image_overlay (text on top of image with gradient), or image_left (image beside text on desktop).aspect_ratioselectImage aspect ratio: square (default), portrait (3:4), landscape (4:3), or auto (original).show_countcheckboxRender the collection's product count below the title. Default on.show_subtitlecheckboxRender the per-card subtitle if provided. Default on.gaprangeGap between cards. 0-60px in 4px steps. Default 20px.padding_toprangeTop padding of the section. 0-160px in 4px steps. Default 60px.padding_bottomrangeBottom padding of the section. 0-160px in 4px steps. Default 60px.Block settings (per card, up to 12 cards):
collectioncollectionRequired. The collection this card links to. Cards with no collection are skipped.image_overrideimage_pickerOptional custom image. Falls back to collection.image, then collection.products.first.featured_image.title_overridetextOptional custom title. Falls back to the collection title.subtitletextOptional tagline shown below the title when show_subtitle is on.cta_labeltextOptional CTA copy (e.g. "Shop now"). Whole card is already clickable.How it works
The grid is a CSS grid with three breakpoints. On mobile it uses columns_mobile (1 or 2), between 750px and 990px it forces 2 columns for tablets, and at 990px and above it uses columns_desktop (2 to 5). Both column counts are passed as CSS custom properties --jlx-cg-cols-d and --jlx-cg-cols-m set inline from Liquid, so the actual column count always matches the picked value.
Each card resolves its image with a three-step fallback: block.settings.image_override if set, otherwise collection.image, otherwise collection.products.first.featured_image. That means a brand-new collection with no products and no image still renders an empty media slot rather than breaking the layout, and a stylist can hand-pick a hero image per card without touching the underlying collection.
Images use Shopify's image_url filter with a responsive srcset at 400w, 600w, 800w, and 1200w. The sizes attribute matches the column count so the browser fetches the smallest viable file. Native loading="lazy" defers offscreen images, and explicit width / height from the source asset prevents layout shift.
Card style is a CSS variant. The outer <section> gets a class like jlx-cg--image_overlay, jlx-cg--image_above, or jlx-cg--image_left, and CSS swaps the layout. Overlay mode positions the body absolutely over the media with a dark gradient and white text. Left mode switches the card to a 40/60 grid at desktop only. Above is the default vertical stack.
Aspect ratio is set with a CSS aspect-ratio declaration on the media slot, scoped per variant class (jlx-cg--ratio-square, jlx-cg--ratio-portrait, etc.). The image inside fills the slot with object-fit: cover, so any source crops cleanly without distortion.
Compatibility
Tested against Dawn 12+ and Horizon, and works on Sense, Craft, Refresh, and most OS 2.0 themes. The section reads --color-foreground, --color-background, --color-background-2, --color-foreground-secondary, and --color-border from the theme so it picks up your brand colours automatically.
On themes that do not define these CSS custom properties, the section falls back to neutral defaults (near-black foreground, white background, light grey muted tones). To force brand colours, override .jlx-cg__title, .jlx-cg__count, and .jlx-cg__cta in the section CSS.
aspect-ratio CSS is supported in Safari 15+, Chrome 88+, and Firefox 89+, which covers every browser Shopify themes target. Shopify's storefront renderer requires a modern browser, so this is not a concern for OS 2.0 themes.
Limitations
- Twelve cards maximum:
max_blocks: 12is set in the schema. Most homepages use 3 to 6. To raise the cap, change the value in the schema. - Five desktop columns maximum: the
columns_desktopselect tops out at 5. Beyond five columns each card becomes too narrow for the title and product count to read comfortably. Add more options to the select if you need a denser layout. - One image per card: the section renders a single image per card. Hover-swap or carousel-in-card behaviour is not built in. Wire up a second
image_pickersetting and a CSS hover rule to add it. - Product count is server-rendered:
collection.products_countreflects the count at render time and does not update if products are added later without a cache bust. Shopify auto-invalidates on collection changes, so this is rarely visible in practice. - Headless storefronts: this is a Liquid section and does not apply to Hydrogen or other headless setups.
Need this built for your store?
If you want the collection grid styled to match your brand, extended with hover swaps, animation, or wired up to merchandised metafields, we can scope and deliver that.