Skip to main content

Sections

Sections are the configurable building blocks of pages. Each section is a self-contained component with its own template, settings, and optional CSS.


Anatomy of a Section

Every section file has three parts:

  1. Liquid template -- the HTML markup
  2. Schema block -- settings definition (JSON)
  3. Style block -- per-section CSS variables (optional)
{%- comment -%} 1. Liquid Template {%- endcomment -%}

{% section_wrapper 'section' class: 'hero' %}
<div class="hero-content">
{% htmltag 'h1' section.settings.title class: 'hero-title' %}
{% htmltag 'p' section.settings.subtitle class: 'hero-subtitle' %}

{% if section.settings.button.href != blank %}
<a href="{{ section.settings.button | link_url }}"
{% if section.settings.button | link_new_tab? %}target="_blank" rel="noopener"{% endif %}
class="btn">
{{ section.settings.button | link_text }}
</a>
{% endif %}
</div>
{% endsection_wrapper %}

{%- comment -%} 2. Style Block (per-section CSS) {%- endcomment -%}

{% style %}
[data-section-wrapper][data-section-id="{{ section.id }}"] {
{% if section.settings.custom_colors and section.settings.bgcolor != blank %}
--section-bgcolor: {{ section.settings.bgcolor | color_alpha_to_rgba }};
{% else %}
--section-bgcolor: var(--theme-background-color);
{% endif %}

background-color: var(--section-bgcolor);
}
{% endstyle %}

{%- comment -%} 3. Schema Block {%- endcomment -%}

{% schema %}
{
"name": "Hero Banner",
"category": "hero",
"settings": [
{
"type": "text",
"id": "title",
"label": "Title"
},
{
"type": "text",
"id": "subtitle",
"label": "Subtitle",
"nb_rows": 2
},
{
"type": "checkbox",
"id": "custom_colors",
"label": "Custom Colors",
"info": "Override theme colors for this section"
},
{
"type": "color_alpha",
"id": "bgcolor",
"label": "Background Color",
"show_if": { "setting": "custom_colors", "eq": true }
},
{
"type": "link",
"id": "button",
"label": "Button",
"with_text": true
}
],
"presets": [
{
"name": "Default",
"settings": {
"title": "Welcome",
"subtitle": "Your story starts here"
}
}
]
}
{% endschema %}

File Organization

Group sections by category inside the sections/ directory:

sections/
├── hero/
│ ├── hero_01.liquid # Full-width hero with image
│ ├── hero_02.liquid # Split hero (text + image)
│ └── hero_03.liquid # Video background hero
├── content/
│ ├── post.liquid # Single post display
│ ├── posts.liquid # Post grid/list
│ └── text_block.liquid # Rich text content
├── cta/
│ ├── cta_01.liquid # Simple CTA
│ └── cta_02.liquid # CTA with image
├── nav/
│ └── main.liquid # Navigation bar
├── header/
│ └── masthead.liquid # Site header/masthead
└── footer/
└── footer_01.liquid # Site footer

Schema Properties

The {% schema %} block defines how your section appears in the editor and what settings are available.

PropertyRequiredDescription
nameYesDisplay name in the section picker
categoryNoGroup in section picker (hero, content, cta, nav, footer, etc.)
tagNoHTML wrapper tag (default: section)
site_scopedNoIf true, section data is shared across all pages (see Site-Scoped Sections)
zonesNoLayout zones this section can be placed in (e.g., ["header"])
settingsYesArray of setting definitions
blocksNoNested block definitions
blocks_presentationNo"list" (flat) or "tree" (nested hierarchy)
presetsNoDefault configurations for initial values
Presets are the source of truth for defaults

Section settings do NOT have a default field in the schema. All initial values are defined in presets. When a section is added to a page, the preset values are used.


Page-Scoped vs Site-Scoped Sections

OQO supports two scoping models for sections.

Page-Scoped Sections (Default)

Each page gets its own copy of the section data. Editing the section on one page does not affect other pages.

This is the default behavior -- no special configuration needed.

{% schema %}
{
"name": "Featured Posts",
"category": "content",
"settings": [
{ "type": "text", "id": "heading", "label": "Heading" }
]
}
{% endschema %}

Site-Scoped Sections

Site-scoped sections share their settings across all pages. Editing the section on any page updates it everywhere. This is ideal for navigation bars, site headers, and footers that should be consistent across the entire site.

To make a section site-scoped, add "site_scoped": true to the schema:

{% schema %}
{
"name": "Main Navigation",
"site_scoped": true,
"zones": ["header"],
"settings": [
{ "type": "image", "id": "logo", "label": "Logo" }
],
"blocks": [
{
"type": "link_item",
"name": "Menu Link",
"settings": [
{ "type": "link", "id": "link", "label": "Link" }
]
}
],
"blocks_presentation": "tree"
}
{% endschema %}

Common Site-Scoped Sections

SectionWhy Site-Scoped
header/mastheadSite title/logo is the same on every page
nav/mainNavigation links are shared across the site
footer/footer_01Footer content is identical everywhere

Section CSS with {% style %} Blocks

The {% style %} block lets you define per-section CSS that updates instantly via hot-reload when settings change in the editor. This is the recommended approach for any CSS that depends on section settings.

How It Works

  1. You write CSS with Liquid inside {% style %}...{% endstyle %} in your section .liquid file
  2. OQO extracts the block, compiles it with the section's settings, and saves it as a separate CSS file
  3. When a user changes a color setting in the editor, only the CSS file is regenerated -- no full page reload needed

Basic Example

{% style %}
[data-section-wrapper][data-section-id="{{ section.id }}"] {
--section-bgcolor: {{ section.settings.bgcolor | default: '#1F2937' }};
--section-text-color: {{ section.settings.text_color | default: '#FFFFFF' }};

background-color: var(--section-bgcolor);
color: var(--section-text-color);
}
{% endstyle %}

The custom_colors Pattern

For sections that support optional color overrides, follow this standardized pattern. A custom_colors checkbox gates the color settings, and each CSS variable checks both the checkbox and the individual setting:

{% style %}
[data-section-wrapper][data-section-id="{{ section.id }}"] {
{% if section.settings.custom_colors and section.settings.bgcolor != blank %}
--section-bgcolor: {{ section.settings.bgcolor | color_alpha_to_rgba }};
{% else %}
--section-bgcolor: var(--theme-background-color);
{% endif %}

{% if section.settings.custom_colors and section.settings.heading_color != blank %}
--section-heading: {{ section.settings.heading_color | color_alpha_to_rgba }};
{% else %}
--section-heading: var(--theme-heading-color);
{% endif %}

{% if section.settings.custom_colors and section.settings.text_color != blank %}
--section-text: {{ section.settings.text_color | color_alpha_to_rgba }};
{% else %}
--section-text: var(--theme-text-color);
{% endif %}

background-color: var(--section-bgcolor);
h2 { color: var(--section-heading); }
p { color: var(--section-text); }
}
{% endstyle %}

The corresponding schema settings:

{
"type": "header",
"content": "Colors"
},
{
"type": "checkbox",
"id": "custom_colors",
"label": "Custom Colors",
"info": "Override theme colors for this section"
},
{
"type": "color_alpha",
"id": "bgcolor",
"label": "Background Color",
"show_if": { "setting": "custom_colors", "eq": true }
},
{
"type": "color_alpha",
"id": "heading_color",
"label": "Heading Color",
"show_if": { "setting": "custom_colors", "eq": true }
},
{
"type": "color_alpha",
"id": "text_color",
"label": "Text Color",
"show_if": { "setting": "custom_colors", "eq": true }
}

Background Image in Style Block

{% style %}
[data-section-wrapper][data-section-id="{{ section.id }}"] {
--section-bg-image: url('{{ section.settings.background_image | image_url: 'hero' }}');
--section-overlay: {{ section.settings.overlay_color | color_alpha_to_rgba }};
}
{% endstyle %}

Rules for {% style %} Blocks

DoDo Not
CSS variables (--section-*)Animations or keyframes
Simple property rulesComplex hover effects
Settings-driven values onlyStatic styles that never change
Use [data-section-wrapper][data-section-id="{{ section.id }}"] selectorUse class-based selectors

Why? Style blocks are re-rendered on every settings change in the editor. Keep them lightweight. Put animations, transitions, and complex component styles in your theme's _sections.css stylesheet instead, scoped under the section's type class (e.g., .gz-hero-hero_01).

Do not use inline styles for settings-driven CSS

Avoid {% capture section_style %} and inline style: attributes for settings. Inline styles cannot be hot-reloaded -- they require a full HTML re-render. Use {% style %} blocks instead.


Section with Posts

Use the global collections accessor to display posts in a section:

{% section_wrapper 'section' class: 'recent-posts' %}
{% htmltag 'h2' section.settings.title class: 'section-title' %}

<div class="posts-grid">
{%- assign collection_slug = section.settings.collection -%}
{% for post in collections[collection_slug] limit: section.settings.count %}
<article class="post-card">
{% if post.image %}
<img src="{{ post.image | image_url: 'medium' }}"
alt="{{ post.image.alt | default: post.title }}">
{% endif %}
<h3><a href="{{ post.url }}">{{ post.title }}</a></h3>
<p>{{ post.excerpt }}</p>
<span>{{ post.published_at | date_format: 'medium' }}</span>
</article>
{% endfor %}
</div>
{% endsection_wrapper %}

{% schema %}
{
"name": "Post Grid",
"category": "content",
"settings": [
{
"type": "text",
"id": "title",
"label": "Section Title"
},
{
"type": "collection",
"id": "collection",
"label": "Collection",
"placeholder": "Select collection...",
"url_mode_available": true
},
{
"type": "number",
"id": "count",
"label": "Number of Posts"
}
],
"presets": [
{
"name": "Default",
"settings": {
"title": "Latest Posts",
"count": 6
}
}
]
}
{% endschema %}

All Posts Shortcut

To show recent posts from across the site without a specific collection:

{% for post in collections.all_posts limit: section.settings.count %}
<article>
<h3><a href="{{ post.url }}">{{ post.title }}</a></h3>
<p>{{ post.excerpt }}</p>
</article>
{% endfor %}

With Pagination

{% paginate collections[collection_slug] by section.settings.count %}
{% for post in paginate.collection %}
<article>{{ post.title }}</article>
{% endfor %}

<nav class="pagination">
{% if paginate.previous %}
<a href="{{ paginate.previous.url }}">&laquo; Previous</a>
{% endif %}
{% for part in paginate.parts %}
{% if part.is_link %}
<a href="{{ part.url }}">{{ part.title }}</a>
{% else %}
<span class="current">{{ part.title }}</span>
{% endif %}
{% endfor %}
{% if paginate.next %}
<a href="{{ paginate.next.url }}">Next &raquo;</a>
{% endif %}
</nav>
{% endpaginate %}

Complete Schema Examples

All Setting Types

Here is a comprehensive schema demonstrating every common setting type:

{% schema %}
{
"name": "Full Example Section",
"category": "content",
"settings": [
{
"type": "header",
"content": "Content"
},
{
"type": "text",
"id": "title",
"label": "Title"
},
{
"type": "text",
"id": "body",
"label": "Body Text",
"nb_rows": 3
},
{
"type": "link",
"id": "button",
"label": "Button",
"with_text": true
},
{
"type": "image",
"id": "background_image",
"label": "Background Image"
},
{
"type": "icon",
"id": "section_icon",
"label": "Icon"
},
{
"type": "header",
"content": "Layout"
},
{
"type": "select",
"id": "columns",
"label": "Columns",
"options": [
{ "value": "2", "label": "2 Columns" },
{ "value": "3", "label": "3 Columns" },
{ "value": "4", "label": "4 Columns" }
]
},
{
"type": "number",
"id": "count",
"label": "Number of Items"
},
{
"type": "range",
"id": "spacing",
"label": "Section Spacing",
"min": 0,
"max": 200,
"step": 10,
"unit": "px"
},
{
"type": "checkbox",
"id": "show_date",
"label": "Show Date"
},
{
"type": "checkbox",
"id": "edge_to_edge",
"label": "Edge to Edge",
"info": "Extend section to viewport edges"
},
{
"type": "header",
"content": "Data Source"
},
{
"type": "collection",
"id": "source_collection",
"label": "Collection",
"placeholder": "Select collection...",
"url_mode_available": true
},
{
"type": "post",
"id": "featured_post",
"label": "Featured Post",
"placeholder": "Select a post..."
},
{
"type": "content",
"id": "source",
"label": "Content Source",
"url_mode_available": true
},
{
"type": "header",
"content": "Colors"
},
{
"type": "checkbox",
"id": "custom_colors",
"label": "Custom Colors",
"info": "Override theme colors for this section"
},
{
"type": "color_alpha",
"id": "bgcolor",
"label": "Background Color",
"show_if": { "setting": "custom_colors", "eq": true }
},
{
"type": "color_alpha",
"id": "heading_color",
"label": "Heading Color",
"show_if": { "setting": "custom_colors", "eq": true }
},
{
"type": "color_alpha",
"id": "text_color",
"label": "Text Color",
"show_if": { "setting": "custom_colors", "eq": true }
},
{
"type": "info",
"content": "Colors above only take effect when 'Custom Colors' is enabled"
}
],
"presets": [
{
"name": "Default",
"settings": {
"title": "Section Title",
"columns": "3",
"count": 6,
"spacing": 60,
"show_date": true,
"edge_to_edge": false,
"custom_colors": false
}
}
]
}
{% endschema %}

Conditional Settings with show_if

Use show_if to show or hide settings based on other setting values:

{
"type": "checkbox",
"id": "use_url_collection",
"label": "Use Collection from URL"
},
{
"type": "collection",
"id": "collection",
"label": "Collection",
"show_if": { "setting": "use_url_collection", "eq": false }
}

The show_if option supports:

  • eq -- show when the referenced setting equals this value
  • neq -- show when the referenced setting does NOT equal this value
{
"type": "select",
"id": "layout",
"label": "Layout",
"options": [
{ "value": "grid", "label": "Grid" },
{ "value": "list", "label": "List" }
]
},
{
"type": "range",
"id": "grid_columns",
"label": "Grid Columns",
"min": 2, "max": 4, "step": 1,
"show_if": { "setting": "layout", "neq": "list" }
}

Blocks

Blocks are repeatable content items within a section -- navigation links, feature cards, testimonials, FAQ items, etc. OQO supports three block patterns, from simple to complex.

Pattern A: Flat Blocks

For simple repeatable content like feature cards, testimonials, or stats. Use "blocks_presentation": "list" (or omit it -- list is the default).

One block type, one level, no nesting.

{% section_wrapper 'section' class: 'features' %}
<div class="features-grid">
{% for block in section.blocks %}
<div class="feature-card">
<i class="{{ block.settings.icon }}"></i>
<h3>{{ block.settings.title }}</h3>
<p>{{ block.settings.description }}</p>
</div>
{% endfor %}
</div>
{% endsection_wrapper %}

{% schema %}
{
"name": "Features",
"category": "content",
"settings": [
{ "type": "text", "id": "heading", "label": "Section Heading" }
],
"blocks": [
{
"type": "feature",
"name": "Feature",
"settings": [
{ "type": "icon", "id": "icon", "label": "Icon" },
{ "type": "text", "id": "title", "label": "Title" },
{ "type": "text", "id": "description", "label": "Description", "nb_rows": 2 }
]
}
]
}
{% endschema %}

Block Type Filtering

When a section has multiple block types, filter by type name:

{% for block in section.blocks.feature %}
<div class="feature-card">{{ block.settings.title }}</div>
{% endfor %}

{% for block in section.blocks.testimonial %}
<blockquote>{{ block.settings.quote }}</blockquote>
{% endfor %}

Type filtering also works on nested children: block.blocks.link_item.

Pattern B: Indented Blocks (User-Controlled Hierarchy)

For hierarchical content where all items are the same type but users control nesting via drag-to-indent in the admin UI. Typical use: navigation links with sublinks, FAQ groups.

Set "blocks_presentation": "tree". The admin UI shows indent/outdent controls. When a user drags a block to the right, it becomes a child of the block above it. Children are accessed via block.blocks.

{% section_wrapper 'nav' class: 'main-nav' %}
<ul class="nav-links">
{% for link in section.blocks.link_item %}
<li>
<a href="{{ link.settings.link | link_url }}">
{{ link.settings.link | link_text }}
</a>

{% if link.blocks.size > 0 %}
<ul class="submenu">
{% for sublink in link.blocks %}
<li>
<a href="{{ sublink.settings.link | link_url }}">
{{ sublink.settings.link | link_text }}
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
{% endsection_wrapper %}

{% schema %}
{
"name": "Simple Nav",
"site_scoped": true,
"zones": ["header"],
"blocks": [
{
"type": "link_item",
"name": "Link",
"settings": [
{ "type": "link", "id": "link", "label": "Link" }
]
}
],
"blocks_presentation": "tree"
}
{% endschema %}

Key points:

  • Single block type -- all items are link_item, but users create parent-child relationships by indenting
  • block.blocks -- gives you the children of any block (populated automatically by the tree builder)
  • Unlimited depth -- for deep nesting, use a recursive snippet (see below)

Recursive Rendering for Deep Nesting

When blocks can nest more than two levels deep, use a recursive snippet:

{%- comment -%} In section template {%- endcomment -%}
{% for link in section.blocks.link_item %}
{% render 'nav_link', link: link, depth: 0 %}
{% endfor %}
{%- comment -%} snippets/nav_link.liquid {%- endcomment -%}
<a href="{{ link.settings.link | link_url }}" class="nav-link depth-{{ depth }}">
{{ link.settings.link | link_text }}
</a>

{% if link.blocks.size > 0 %}
<ul class="submenu">
{% for child in link.blocks %}
<li>{% render 'nav_link', link: child, depth: depth | plus: 1 %}</li>
{% endfor %}
</ul>
{% endif %}

Pattern C: Nested Blocks (Schema-Defined Types)

For complex structures with different block types in a strict parent-child hierarchy. Each type declares which child types it accepts via the accept array.

Typical use: footer with columns containing links, tabs with content panels, menus with link groups and buttons.

{% section_wrapper 'footer' class: 'site-footer' %}
<div class="footer-columns">
{% for column in section.blocks.column %}
<div class="footer-col">
<h4>{{ column.settings.title }}</h4>
<ul>
{% for link in column.blocks.link_item %}
<li>
<a href="{{ link.settings.link | link_url }}">
{{ link.settings.link | link_text }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
{% endsection_wrapper %}

{% schema %}
{
"name": "Footer",
"site_scoped": true,
"zones": ["footer"],
"blocks": [
{
"type": "column",
"name": "Column",
"accept": ["link_item"],
"limit": 4,
"settings": [
{ "type": "text", "id": "title", "label": "Column Title" }
]
},
{
"type": "link_item",
"name": "Link",
"root": false,
"settings": [
{ "type": "link", "id": "link", "label": "Link" }
]
}
],
"blocks_presentation": "tree"
}
{% endschema %}

Key points:

  • accept -- column blocks accept link_item children. The admin UI only allows valid child types.
  • root: false -- link_item cannot be added at the top level; it must be inside a column.
  • limit -- maximum instances of this block type per section.

Mixing Patterns B and C

Patterns B and C combine naturally. Define multiple parent types with accept, and let same-type items nest via indentation:

{% schema %}
{
"name": "Navigation",
"site_scoped": true,
"zones": ["header"],
"blocks": [
{
"type": "link_group",
"name": "Links",
"accept": ["link_item"],
"limit": 1,
"settings": [
{ "type": "text", "id": "title", "label": "Title" }
]
},
{
"type": "link_item",
"name": "Link",
"settings": [
{ "type": "link", "id": "link", "label": "Link" }
]
},
{
"type": "button_group",
"name": "Icon Buttons",
"accept": ["button_item"],
"limit": 1,
"settings": []
},
{
"type": "button_item",
"name": "Icon Button",
"settings": [
{ "type": "icon", "id": "icon", "label": "Icon" },
{ "type": "link", "id": "link", "label": "Link" }
]
}
],
"blocks_presentation": "tree"
}
{% endschema %}

Here link_group accepts link_item children (Pattern C), and users can indent link_item blocks under other link_item blocks to create multi-level menus (Pattern B). The template handles both:

{% for block in section.blocks %}
{% if block.type == 'link_group' %}
<nav>
{% for link in block.blocks %}
{% render 'nav_link', link: link, depth: 0 %}
{% endfor %}
</nav>
{% endif %}
{% if block.type == 'button_group' %}
<div class="icon-buttons">
{% for btn in block.blocks %}
<a href="{{ btn.settings.link | link_url }}">
<i class="{{ btn.settings.icon }}"></i>
</a>
{% endfor %}
</div>
{% endif %}
{% endfor %}

Pattern Comparison

AspectA: FlatB: IndentedC: Nested
Use caseFeature cards, statsNav with sublinksFooter columns, tabs
blocks_presentation"list" (default)"tree""tree"
Block types112+
Parent-childNoneUser controls via indentSchema defines via accept
Nesting depthFlatUnlimitedDefined by schema
Template accesssection.blocksblock.blocks (recursive)block.blocks.type

Block Schema Properties

PropertyDescription
typeUnique identifier for the block type
nameDisplay name in admin UI
acceptArray of child block types this block can contain
rootIf false, block cannot be added at root level (must have a parent)
limitMaximum number of this block type per section
settingsArray of settings for the block

Section Wrapper

Always use {% section_wrapper %} to wrap your section content. It adds the data attributes needed for the editor and CSS scoping.

{%- comment -%} Default: renders a <div> {%- endcomment -%}
{% section_wrapper class: 'my-section' %}
<h2>Content</h2>
{% endsection_wrapper %}

{%- comment -%} Specify element type {%- endcomment -%}
{% section_wrapper 'nav' class: 'main-nav' %}
<a href="/">Home</a>
{% endsection_wrapper %}

{%- comment -%} Footer {%- endcomment -%}
{% section_wrapper 'footer' class: 'site-footer' %}
<p>&copy; {{ site.name }}</p>
{% endsection_wrapper %}

Output

<div class="gz-section gz-content-my_section my-section"
data-section-wrapper="true"
data-section-id="S8vQP-jI"
data-section-type="content/my_section">
<h2>Content</h2>
</div>
AttributePurpose
gz-sectionCommon class for all sections
gz-{category}-{name}Section-type class for CSS targeting
data-section-wrapperMarker for the editor and CSS scoping
data-section-idUnique instance ID for this section
data-section-typeSection template path

Parameters

ParameterDescription
First argumentHTML element: 'div', 'section', 'nav', 'header', 'footer', etc.
classCSS classes to add
idHTML id attribute
Any data-*Custom data attributes

Best Practices

  1. Always use section_wrapper -- it provides the data attributes needed for the editor, live preview, and CSS scoping.

  2. Use {% style %} blocks for settings-driven CSS -- they enable instant hot-reload of color and spacing changes.

  3. Keep {% style %} blocks lightweight -- only CSS variables and simple property rules. Put animations and complex styles in your theme stylesheet.

  4. Provide presets -- sections should look good out of the box. Define sensible initial values in presets.

  5. Group settings with headers -- use "type": "header" to organize related settings visually.

  6. Use show_if for conditional settings -- hide settings that are irrelevant based on other selections.

  7. Use color_alpha for section colors -- always with the custom_colors checkbox pattern and color_alpha_to_rgba filter.

  8. Use semantic CSS variable names -- prefix section variables with --section-* to distinguish from --theme-* global variables.