Shopify Theme Development
Develop your theme focused on scalability and reusability
cursor.directory·↓ 9
规则
Shopify Reusable Snippets
A reference for building scalable, easy-to-use Liquid snippets
# How to Create Reusable Shopify Snippets
A reference for building scalable, easy-to-use Liquid snippets. Use these patterns when creating heading, CTA, announcement, image, and other reusable components.
## When to Create a Snippet
Create a snippet when:
- The same UI appears in 3+ sections or templates
- Logic is complex enough to warrant isolation
- You need consistency (e.g. all CTAs look the same)
- Schema settings would be duplicated across sections
Avoid snippets for:
- One-off layouts
- Content that varies wildly by context
- Simple output (< 5 lines)
## Snippet Documentation Format
Use structured `{% comment %}` blocks at the top of every snippet:
```liquid
{% comment %}
===================================================================================
SNIPPET NAME
===================================================================================
Brief description of what the snippet does and when to use it.
********************************************
Parameters
********************************************
* param_name (Type): Description
* optional_param (Type, Optional): Description. Default: value
********************************************
Usage
********************************************
{% render 'snippet-name', param: value, optional: value %}
********************************************
Schema (for section settings that pass into snippet)
********************************************
{ "type": "text", "id": "param_id", "label": "Label", "visible_if": "{{ section.settings.enable }}" }
{% endcomment %}
```
## Parameter Naming Conventions
### Kebab-case for Multi-word Parameters
Use kebab-case when the parameter name has multiple words:
```liquid
{% render 'section-heading'
title: ss.heading_title,
sub_text: ss.heading_sub_text,
align: ss.heading_align,
align_mb: ss.heading_align_mb,
shop_all: ss.heading_shop_all
%}
```
### Action-based Select (vs Multiple Booleans)
For mutually exclusive behaviors, use a single `action` param instead of multiple booleans:
```liquid
{% render 'cta-button'
text: ss.cta_text,
link: product.url,
type: ss.cta_type,
action: ss.cta_action,
variant_id: product.selected_or_first_available_variant.id,
note: ss.cta_note,
note_icon: ss.cta_note_icon
%}
```
Schema: `action` select with options like `""` (link), `"atc"` (add to cart), `"scroll"` (scroll to product).
### Consistent Parameter Order
Order parameters by: enable flag → content → styling → optional extras. Keeps calls readable.
## Conditional Rendering
### Wrapper Conditionals
Wrap the entire output when the snippet may render nothing:
```liquid
{% if enable %}
<div class="section-heading">
<!-- Content -->
</div>
{% endif %}
```
### Element-level Conditionals
Check blanks before rendering individual elements:
```liquid
{% if title != blank %}
<h2 class="section-heading__title">{{ title }}</h2>
{% endif %}
{% if text != blank %}
<div class="section-heading__text">{{ text }}</div>
{% endif %}
```
### Early Skip
For complex snippets, wrap everything in a single top-level conditional:
```liquid
{% if enable and (title != blank or text != blank) %}
<div class="section-heading">
<!-- Content -->
</div>
{% endif %}
```
Liquid has no early return; use a single wrapping conditional instead.
## Variable Assignments
### Default Values
Use `default` filter for fallbacks:
```liquid
{% assign heading_tag = h1 | default: false %}
{% assign heading_tag = heading_tag | replace: 'true', '1' | replace: 'false', '2' | prepend: 'h' %}
```
### Class Concatenation
Build class strings from parameters:
```liquid
{% assign classes = variant | append: ' ' | append: class | strip %}
<div class="cta-button {{ classes }}">
```
### Liquid Block for Multiple Assignments
Use `{% liquid %}` for cleaner multi-step logic:
```liquid
{% liquid
assign product = product | default: section.settings.product
assign link = link | default: product.url
assign variant_id = variant_id | default: product.selected_or_first_available_variant.id
%}
```
### String Manipulation for Derived Values
Extract link title from URL for `title` attribute:
```liquid
{% assign link_title = link | split: '?' | first | split: '//' | last | split: '/' | slice: 1, 2 | join: ' ' | capitalize %}
```
## Dynamic HTML Elements
### Dynamic Tag Names
Use variables for tag names (e.g. h1 vs h2):
```liquid
{% assign tag = h1 | default: false | replace: 'true', '1' | replace: 'false', '2' | prepend: 'h' %}
<{{ tag }} class="section-heading__title">{{ title }}</{{ tag }}>
```
### Configurable Element Type
Allow link vs button based on context:
```liquid
{% assign el = element | default: 'a' %}
<{{ el }} href="{{ link }}" class="cta-button">
{{ text }}
</{{ el }}>
```
## Multiple Rendering Modes
### Action-based Branching
Use `action` param for mutually exclusive behaviors. Keeps schema simple (one select vs multiple checkboxes):
```liquid
{% if scroll or action == 'scroll' %}
<div class="cta-button {{ type }}" data-scroll-to="product" aria-label="Scroll to product">
{% if text != blank %}{{ text }}{% else %}{{ 'products.product.shop_now' | t }}{% endif %}
</div>
{% elsif action == 'atc' and variant_id != blank %}
<div class="cta-button {{ type }}" data-variant-id="{{ variant_id }}" aria-label="Add to Cart">
{% if text != blank %}{{ text }}{% else %}{{ 'products.product.add_to_cart' | t }}{% endif %}
</div>
{% else %}
<a href="{% if link != blank %}{{ link }}{% else %}/{% endif %}" title="{{ link_title }}" class="cta-button {{ type }}">
{% if text != blank %}{{ text }}{% else %}{{ 'products.product.shop_now' | t }}{% endif %}
</a>
{% endif %}
```
Add-to-cart uses a `div` with `data-variant-id`; JavaScript handles the form submission. No `<form>` in snippet.
### Captured Auxiliary Content
Use `{% capture %}` for optional content reused across all modes (e.g. CTA note below button):
```liquid
{% capture cta_note %}
{% if note != blank %}
<div class="cta-note">
{% if note_icon != blank %}
{{ note_icon | image_url: width: 30 | image_tag: loading: 'lazy', alt: '' }}
{% endif %}
<p>{{ note }}</p>
</div>
{% endif %}
{% endcapture %}
{% if action == 'scroll' %}
<div class="cta-button {{ type }}">...</div>
{{ cta_note }}
{% elsif action == 'atc' %}
<div class="cta-button {{ type }}">...</div>
{{ cta_note }}
{% else %}
<a href="{{ link }}" class="cta-button {{ type }}">...</a>
{{ cta_note }}
{% endif %}
```
### Translation Fallbacks
Use locale key when text is blank:
```liquid
{% if text != blank %}{{ text }}{% else %}{{ 'products.product.add_to_cart' | t }}{% endif %}
```
## Nested Snippet Rendering
### Conditional Nesting
Render child snippets only when relevant:
```liquid
{% if show_ratings %}
{% if ratings_type == 'custom' %}
{% render 'ratings-custom', stars: ratings_stars, logo: ratings_logo, text: ratings_text %}
{% else %}
{% render 'ratings-widget', type: ratings_type %}
{% endif %}
{% endif %}
```
### Pass-through Parameters
Forward all needed params to nested snippets. Avoid passing whole objects when a few values suffice:
```liquid
{% render 'countdown-timer'
enabled: timer,
type: timer_type,
end_date: timer_date,
timezone: timer_timezone,
format: timer_format,
class: 'section-heading__timer'
%}
```
## Block Iteration
When sections pass blocks into a snippet:
```liquid
{% if items.size > 0 %}
<div class="section-heading__items">
{% for item in items %}
<div class="section-heading__item" {{ item.shopify_attributes }}>
{% if item.settings.icon %}
{{ item.settings.icon | image_url: width: 60 | image_tag: loading: 'lazy', alt: item.settings.text }}
{% endif %}
<p>{{ item.settings.text }}</p>
</div>
{% endfor %}
</div>
{% endif %}
```
## Data Attributes for JavaScript
Use data attributes instead of inline scripts:
```liquid
<div class="countdown-timer {{ class }}"
data-type="{{ type }}"
data-end-date="{{ end_date }}"
{% if timezone %}data-timezone="{{ timezone }}"{% endif %}
{% if format %}data-format="{{ format }}"{% endif %}>
<span class="countdown-timer__display">00:00:00</span>
</div>
```
JavaScript reads these and initializes behavior. Keeps snippet portable.
## Inline Styles for Dynamic Values
Use inline styles for colors/sizes passed from schema:
```liquid
<div class="announcement-bar" style="background-color: {{ bg_color }}; color: {{ text_color }};">
<!-- Content -->
</div>
```
## Case Statements
Use `case` for multiple variants:
```liquid
{% case variant %}
{% when 'accent' %}
{% assign btn_class = 'cta-button--accent' %}
{% when 'outline' %}
{% assign btn_class = 'cta-button--outline' %}
{% else %}
{% assign btn_class = 'cta-button--primary' %}
{% endcase %}
```
## Class Variants
Support variants via parameter concatenation:
```liquid
{% assign classes = variant | append: ' ' | append: class | strip %}
<div class="cta-button {{ classes }}">
```
Conditional modifier:
```liquid
<div class="section-heading__text{% if read_more %} section-heading__text--expandable{% endif %}">{{ text }}</div>
```
## Object Access
Pass objects when the snippet needs them. Derive values with fallbacks:
```liquid
{% liquid
assign product = product | default: section.settings.product
assign link = link | default: product.url
assign variant_id = variant_id | default: product.selected_or_first_available_variant.id
%}
```
For collection links:
```liquid
{% if shop_all != blank %}
<a href="{{ shop_all.url }}" class="section-heading__shop-all">{{ shop_all.title }}</a>
{% endif %}
```
## Icon and Image Rendering
Conditional icon:
```liquid
{% if show_icon %}{% render 'icon-arrow' %}{% endif %}
```
If your theme has an image snippet, use it. Otherwise use built-in filters:
```liquid
{% if image %}
{% assign img_width = width | default: 400 %}
{{ image | image_url: width: img_width | image_tag: loading: 'lazy', alt: alt | default: '' }}
{% endif %}
```
## Snippet Types (Reference Examples)
### Heading Snippet
- **Purpose**: Reusable section heading (title, subtitle, alignment, optional link)
- **Params**: `title`, `sub_text`, `align`, `align_mb`, `shop_all`, `h1`, `class`
- **Pattern**: Wrapper conditional, dynamic tag, element-level blanks
### CTA Snippet
- **Purpose**: Links, scroll-to-product, or add-to-cart with optional note
- **Params**: `text`, `link`, `type` (style variant), `action` (`""` | `"atc"` | `"scroll"`), `variant_id`, `product` (fallback for link/variant_id), `note`, `note_icon`
- **Pattern**: Action-based branching, `{% capture %}` for note, translation fallbacks, data attributes for ATC/scroll (JS handles submit)
- **Add-to-cart**: Use `div` with `data-variant-id`; theme JS intercepts and submits AJAX form
### Announcement Bar Snippet
- **Purpose**: Top bar with text, optional timer, colors from schema
- **Params**: `text`, `bg_color`, `text_color`, `timer`, `timer_*` (pass-through)
- **Pattern**: Inline styles, nested timer snippet, wrapper conditional
### Image Snippet
- **Purpose**: Consistent image output with lazy load, alt, sizes
- **Params**: `image`, `width`, `alt`, `loading`, `class`
- **Pattern**: Blank check, default width, optional class
### Countdown/Timer Snippet
- **Purpose**: JS-driven countdown with config from data attributes
- **Params**: `type`, `end_date`, `timezone`, `format`, `class`
- **Pattern**: Data attributes, no inline JS, placeholder display text
## Schema Integration
When sections use snippets, schema should mirror snippet params:
1. **Header** to group related settings
2. **Checkbox** for enable/disable
3. **Text/richtext** for content
4. **Select** for variant/type/action
5. **Color** for dynamic colors
6. **URL** for links
7. **image_picker** for optional icons (e.g. CTA note icon)
8. **visible_if** – **Required on every setting** that depends on an enable/parent checkbox
### CTA Schema Example
```json
{
"type": "header",
"content": "CTA"
},
{
"type": "checkbox",
"id": "show_cta",
"label": "Show CTA",
"default": true
},
{
"type": "select",
"id": "cta_action",
"label": "CTA Action",
"default": "atc",
"visible_if": "{{ section.settings.show_cta }}",
"options": [
{ "value": "", "label": "Link" },
{ "value": "atc", "label": "Add to Cart" },
{ "value": "scroll", "label": "Scroll to Product" }
]
},
{
"type": "select",
"id": "cta_type",
"label": "CTA Style",
"default": "accent",
"visible_if": "{{ section.settings.show_cta }}",
"options": [
{ "value": "light", "label": "Light" },
{ "value": "dark", "label": "Dark" },
{ "value": "accent", "label": "Accent" }
]
},
{
"type": "text",
"id": "cta_text",
"label": "CTA Text",
"default": "Add to Cart",
"visible_if": "{{ section.settings.show_cta }}"
},
{
"type": "text",
"id": "cta_note",
"label": "CTA Note",
"visible_if": "{{ section.settings.show_cta }}"
},
{
"type": "image_picker",
"id": "cta_note_icon",
"label": "CTA Note Icon",
"visible_if": "{{ section.settings.show_cta }}"
}
```
### Heading Schema Example
```json
{
"type": "header",
"content": "Heading"
},
{
"type": "checkbox",
"id": "heading",
"label": "Show Heading",
"default": true
},
{
"type": "text",
"id": "heading_title",
"label": "Title",
"visible_if": "{{ section.settings.heading }}"
},
{
"type": "text",
"id": "heading_sub_text",
"label": "Subtitle",
"visible_if": "{{ section.settings.heading }}"
},
{
"type": "select",
"id": "heading_align",
"label": "Alignment",
"default": "center",
"visible_if": "{{ section.settings.heading }}",
"options": [
{ "value": "start", "label": "Start" },
{ "value": "center", "label": "Center" },
{ "value": "end", "label": "End" }
]
}
```
## Best Practices
1. **Wrap in conditionals** when snippet might output nothing
2. **Use kebab-case** for multi-word parameters
3. **Provide defaults** with `default` filter (`product | default: section.settings.product`)
4. **Check blank** before rendering content
5. **Document params** in structured comment blocks
6. **Use action select** instead of multiple booleans for mutually exclusive modes
7. **Use `{% capture %}`** for auxiliary content (notes, badges) reused across branches
8. **Use data attributes** for JS (ATC, scroll); keep snippets free of inline scripts
9. **Use translation fallbacks** when text is blank: `{% if text != blank %}{{ text }}{% else %}{{ 'key' | t }}{% endif %}`
10. **Support class/variant params** for styling flexibility
11. **Use inline styles** only for schema-driven values (colors, etc.)
12. **Use `case`** for 3+ conditional branches
13. **Keep snippets focused** – one clear responsibility per file
14. **Avoid deep nesting** – 2–3 levels max; extract if deeper
15. **Name consistently** – `section-heading`, `cta-button`, `announcement-bar`
16. **Add visible_if to every setting** – All settings under an enable checkbox must have `visible_if` referencing that checkbox规则
Shopify Rich Schema Sections
Universal conventions for building Shopify theme sections with blocks, schema, and Liquid. Works with any theme setup
# Rich Shopify Schema Sections Conventions
Universal conventions for building Shopify theme sections with blocks, schema, and Liquid. Works with any theme setup.
## Liquid Variable Assignments
### Section Settings Shorthand
Always assign section settings to a shorthand variable at the top of the file:
```liquid
{% assign ss = section.settings %}
```
### Section Selector Variable
Create a section selector variable for CSS scoping:
```liquid
{% assign sc = '#shopify-section-' | append: section.id %}
```
### Block Settings Shorthand
When iterating over blocks, assign block settings to a shorthand variable:
```liquid
{% for block in section.blocks %}
{% assign bs = block.settings %}
<!-- Use bs instead of block.settings -->
{% endfor %}
```
### Block Filtering by Type
Filter blocks by type using the `where` filter. Use `first` to get a single block, or iterate over the filtered array:
```liquid
{% liquid
assign faqs = section.blocks | where: 'type', 'faq'
assign hero_block = section.blocks | where: 'type', 'hero' | first
assign cards = section.blocks | where: 'type', 'card'
%}
```
### Default Fallbacks
Use the `default` filter for fallback values:
```liquid
{% assign image_mb = ss.image_mb | default: ss.image %}
{% assign bg_image_mb = ss.bg_image_mb | default: ss.bg_image %}
```
## HTML Structure
### Root Container
Use a `<div>` with a section-specific class as the root wrapper:
```liquid
<div class="feature-cards" {{ block.shopify_attributes }}>
<!-- Content -->
</div>
```
For dynamic styling, use inline styles when needed:
```liquid
<div class="feature-cards" style="background-color: {{ ss.bg_color }};">
<!-- Content -->
</div>
```
### Semantic HTML
Use standard semantic elements for layout:
- `<div>` with flex/grid classes for layout containers
- `<section>`, `<article>`, `<header>` for semantic grouping
- `<ul>`/`<li>` for list content (e.g. FAQ, feature lists)
- `<a>` for links
```liquid
<div class="feature-cards__wrapper">
<div class="feature-cards__content">
<div class="feature-cards__items">
<!-- Cards -->
</div>
</div>
<div class="feature-cards__image">
<!-- Image -->
</div>
</div>
```
### BEM Naming Convention
Follow BEM (Block Element Modifier) naming:
- **Block**: Component name (e.g. `feature-cards`)
- **Element**: `feature-cards__item`, `feature-cards__title`, `feature-cards__content`
- **Modifier**: Use `--` prefix (e.g. `feature-cards--compact`)
### Conditional Rendering
Always check for blank values before rendering:
```liquid
{% if bs.subtitle != blank %}
<div class="feature-cards__subtitle">{{ bs.subtitle }}</div>
{% endif %}
```
### Snippet Rendering
Use `{% render %}` for reusable snippets when your theme provides them. Pass all necessary parameters:
```liquid
{% render 'section-heading',
title: ss.heading_title,
alignment: ss.heading_align,
subtitle: ss.heading_subtitle
%}
```
If your theme does not have a heading snippet, output the HTML directly in the section.
## CSS Styling
### Scoped Styles
Use `{% style %}` tag for section-specific styles that reference settings:
```liquid
{% style %}
{{ sc }} {
background-color: {{ ss.bg_color }};
}
{{ sc }} .feature-cards__item {
--accent-color: {{ ss.accent_color }};
background-color: {{ ss.card_bg_color }};
}
{% endstyle %}
```
### CSS Variables
Define CSS variables in the style tag for dynamic values, then reference in CSS:
```liquid
{{ sc }} .feature-cards__item {
--accent-color: {{ ss.accent_color }};
}
```
```css
.feature-cards__item-title strong {
color: var(--accent-color, #000);
}
```
### Inline Styles
Use inline styles for dynamic block-level values:
```liquid
<div class="feature-cards__progress" style="background-color: {{ bs.progress_bg_color }};">
<div class="feature-cards__progress-bar" style="width: {{ bs.progress_num }}%; background-color: {{ bs.progress_color }};">
{{ bs.progress_num }}% {{ bs.progress_text }}
</div>
</div>
```
### Conditional Style Tags
Use `<style>` tag for conditional responsive styles (not `{% style %}`):
```liquid
{% if ss.content_first or ss.content_first_mb %}
<style>
{% if ss.content_first_mb %}
@media (max-width: 767px) {
{{ sc }} .feature-cards__content {
order: -1;
}
}
{% endif %}
{% if ss.content_first %}
@media (min-width: 768px) {
{{ sc }} .feature-cards__content {
order: -1;
}
}
{% endif %}
</style>
{% endif %}
```
### Complex Style Logic
Use Liquid logic within style tags for calculations:
```liquid
<style>
{{ sc }} .hero__card {
{% assign opacity = ss.card_bg_opacity | divided_by: 100.0 %}
background-color: {{ ss.card_bg_color | color_modify: 'alpha', opacity }};
}
{{ sc }} .hero__title {
max-width: {{ ss.title_max_width }}px;
font-family: {{ ss.title_font }};
font-weight: {{ ss.title_font_weight }};
}
@media (min-width: 1024px) {
{{ sc }} .hero {
height: {{ ss.background_height }}px;
}
}
</style>
```
## Schema Structure
### Section Name
Use descriptive, capitalized names:
```json
{
"name": "Feature Cards"
}
```
### Settings Organization
Group related settings with headers:
```json
{
"type": "header",
"content": "Heading"
}
```
### Setting Types
#### Color Pickers
Always provide defaults:
```json
{
"type": "color",
"id": "bg_color",
"label": "Background Color",
"default": "#ffffff"
}
```
#### Checkboxes
Use for boolean toggles:
```json
{
"type": "checkbox",
"id": "heading_h1",
"label": "Heading as H1"
}
```
#### Select Dropdowns
Provide clear, short labels:
```json
{
"type": "select",
"id": "heading_align",
"label": "Heading Alignment",
"default": "center",
"options": [
{ "value": "start", "label": "Start" },
{ "value": "center", "label": "Center" },
{ "value": "end", "label": "End" }
]
}
```
#### Range Inputs
Specify min, max, step, and default:
```json
{
"type": "range",
"id": "progress_num",
"label": "Progress Percentage",
"default": 80,
"min": 0,
"max": 100,
"step": 1
}
```
#### Rich Text
Use for formatted text with optional info hints:
```json
{
"type": "richtext",
"id": "spec_title",
"label": "Specification Title",
"info": "Use bold to emphasize with accent color",
"default": "<p>Feature <strong>highlight</strong></p>"
}
```
#### Textarea
Use for longer text without formatting:
```json
{
"type": "textarea",
"id": "description",
"label": "Description",
"default": "Default description text"
}
```
#### Video
Use for video picker:
```json
{
"type": "video",
"id": "video",
"label": "Background Video (Optional)"
}
```
#### Image Picker
Use for image selection:
```json
{
"type": "image_picker",
"id": "image",
"label": "Image"
}
```
#### URL
Use for link inputs:
```json
{
"type": "url",
"id": "cta_url",
"label": "CTA Link"
}
```
#### Collection
Use for collection picker:
```json
{
"type": "collection",
"id": "collection",
"label": "Collection"
}
```
### Conditional Settings (visible_if)
Use `visible_if` to show settings conditionally:
```json
{
"type": "text",
"id": "heading_title",
"label": "Heading Title",
"visible_if": "{{ section.settings.heading }}"
}
```
Chain multiple conditions:
```json
{
"type": "select",
"id": "ratings_type",
"label": "Ratings Type",
"options": [
{ "value": "custom", "label": "Custom" },
{ "value": "widget", "label": "Widget" }
],
"visible_if": "{{ section.settings.show_ratings }}"
}
```
### Blocks Configuration
#### Block Type and Name
Use descriptive names with kebab-case for type:
```json
{
"type": "card",
"name": "Card",
"limit": 3
}
```
#### Multiple Block Types
Sections can have multiple block types:
```json
{
"blocks": [
{
"type": "video-item",
"name": "Video",
"settings": [
{
"type": "video",
"id": "video",
"label": "Video"
}
]
},
{
"type": "heading-block",
"name": "Heading",
"limit": 2,
"settings": [
{
"type": "text",
"id": "title",
"label": "Title"
}
]
}
]
}
```
#### Block Settings
Follow same patterns as section settings, organized logically.
### Presets
Always include a preset. Optionally include default settings and blocks:
```json
{
"presets": [
{
"name": "Feature Cards",
"category": "Content"
}
]
}
```
With default settings and blocks:
```json
{
"presets": [
{
"name": "FAQ Section",
"settings": {
"bg_color": "#f7f7f7",
"heading": true,
"heading_title": "FREQUENTLY ASKED QUESTIONS",
"cta_url": "shopify://collections/all"
},
"blocks": [
{
"type": "faq",
"settings": {
"title": "Question title?",
"text": "<p>Answer text here</p>"
}
}
]
}
]
}
```
## Block Iteration Patterns
### Basic Loop
Iterate over blocks and assign settings:
```liquid
{% for block in section.blocks %}
{% assign bs = block.settings %}
<div class="feature-cards__item" {{ block.shopify_attributes }}>
<!-- Block content -->
</div>
{% endfor %}
```
### Conditional Block Features
Check for feature flags before rendering:
```liquid
{% if bs.progress %}
<div class="feature-cards__progress">
<!-- Progress bar content -->
</div>
{% endif %}
```
### Filtered Block Iteration
Iterate over filtered blocks:
```liquid
{% assign faqs = section.blocks | where: 'type', 'faq' %}
{% if faqs.size > 0 %}
<ul class="faq-section__list">
{% for faq in faqs %}
<li class="faq-section__item" {{ faq.shopify_attributes }}>
{{ faq.settings.title }}
</li>
{% endfor %}
</ul>
{% endif %}
```
### Multiple Block Type Handling
Handle different block types separately:
```liquid
{% assign videos = section.blocks | where: 'type', 'video-item' %}
{% assign heading_blocks = section.blocks | where: 'type', 'heading-block' %}
{% if heading_blocks.size > 0 %}
<div class="video-section__headings">
{% for block in heading_blocks %}
<div class="video-section__heading" {{ block.shopify_attributes }}>
<h4>{{ block.settings.title }}</h4>
<p>{{ block.settings.text }}</p>
</div>
{% endfor %}
</div>
{% endif %}
<div class="video-section__videos">
{% for video in videos %}
<div class="video-section__video" {{ video.shopify_attributes }}>
<!-- Video content -->
</div>
{% endfor %}
</div>
```
### Image Rendering
Use Shopify's built-in filters for images:
```liquid
{% if bs.icon %}
<div class="feature-cards__icon">
{{ bs.icon | image_url: width: 200 | image_tag: loading: 'lazy', alt: bs.icon_alt | default: 'Icon' }}
</div>
{% endif %}
```
For responsive images with mobile/desktop variants, use `default` for fallback:
```liquid
{% assign image_mb = ss.image_mb | default: ss.image %}
{% if image_mb %}
{% if ss.image and ss.image_mb %}
<picture>
<source media="(min-width: 768px)" srcset="{{ ss.image | image_url: width: 840 }}">
{{ image_mb | image_url: width: 600 | image_tag: loading: 'lazy' }}
</picture>
{% else %}
{{ image_mb | image_url: width: 840 | image_tag: loading: 'lazy' }}
{% endif %}
{% endif %}
```
If your theme provides a `responsive-image` or `responsive-picture` snippet, use that instead.
### Video Rendering
Use the `video_tag` filter for video elements:
```liquid
{% if ss.video %}
{% if ss.bg_preload %}
{% assign video_preload = 'metadata' %}
{% else %}
{% assign video_preload = 'none' %}
{% endif %}
<div class="hero__video">
{{ ss.video | video_tag: controls: false, autoplay: true, loop: true, muted: true, playsinline: true, preload: video_preload }}
</div>
{% endif %}
```
For video blocks:
```liquid
{% if video.settings.video %}
{{ video.settings.video | video_tag: autoplay: false, loop: false, muted: true, controls: true }}
{% endif %}
```
## CTA Rendering
Wrap CTAs in a wrapper div for consistent spacing:
```liquid
{% if ss.cta and ss.cta_url != blank %}
<div class="feature-cards__cta-wrapper">
<a href="{{ ss.cta_url }}" class="button button--{{ ss.cta_style }}">
{{ ss.cta_text | default: 'Learn more' }}
</a>
</div>
{% endif %}
```
If your theme has a CTA/button snippet (e.g. `main-cta`, `button`), use `{% render %}` and pass the parameters your snippet expects.
## Conditional Classes
Add conditional classes based on settings:
```liquid
<div class="cards-section__grid {{ ss.layout }}{% if ss.enable_carousel %} cards-section__grid--carousel{% endif %}">
<!-- Content -->
</div>
```
## CSS Patterns
### Component Structure
- Use BEM naming consistently
- Mobile-first responsive design
- Use CSS variables for dynamic colors
- Define variables in `{% style %}` tag, reference in global CSS
### Responsive Breakpoints
Follow standard breakpoints:
- Mobile: default (no media query)
- Tablet: `@media screen and (min-width: 768px)`
- Desktop: `@media screen and (min-width: 1024px)`
- Large Desktop: `@media screen and (min-width: 1280px)`
### Card Layout Example
```css
.feature-cards__items {
display: flex;
flex-direction: column;
gap: 48px;
}
@media screen and (min-width: 1024px) {
.feature-cards__items {
flex-direction: row;
justify-content: space-evenly;
align-items: stretch;
gap: 24px;
}
.feature-cards__item {
flex: 1;
}
}
```
## Icon Rendering
If your theme provides icon snippets, use them:
```liquid
<div class="video-button play">
{% render 'icon-play' %}Play
</div>
```
Otherwise use inline SVG, icon fonts, or image pickers. Avoid hardcoding icons that merchants cannot customize.
## Best Practices
1. **Always check for blank values** before rendering content
2. **Use shorthand variables** (`ss`, `bs`) for cleaner code
3. **Group related settings** with headers in schema
4. **Provide defaults** for all settings
5. **Use descriptive labels** in schema (avoid redundancy with type)
6. **Scope CSS** using section selector variable (`sc`)
7. **Use CSS variables** for dynamic colors referenced in global CSS
8. **Use inline styles** for block-level dynamic values
9. **Include `{{ block.shopify_attributes }}`** on block root elements for theme editor
10. **Follow BEM naming** consistently throughout component
11. **Filter blocks by type** when sections have multiple block types
12. **Use `default` filter** for fallback values (especially mobile images)
13. **Use conditional `<style>` tags** for responsive conditional styles
14. **Use `visible_if`** in schema for progressive disclosure of settings
15. **Include default settings in presets** for better merchant experience
16. **Check block size** before iterating: `{% if faqs.size > 0 %}`
17. **Use `video_tag` filter** with appropriate attributes for video elements
18. **Prefer built-in Liquid filters** (`image_url`, `image_tag`) unless your theme has standardized snippets