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:
- Liquid template -- the HTML markup
- Schema block -- settings definition (JSON)
- 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.
| Property | Required | Description |
|---|---|---|
name | Yes | Display name in the section picker |
category | No | Group in section picker (hero, content, cta, nav, footer, etc.) |
tag | No | HTML wrapper tag (default: section) |
site_scoped | No | If true, section data is shared across all pages (see Site-Scoped Sections) |
zones | No | Layout zones this section can be placed in (e.g., ["header"]) |
settings | Yes | Array of setting definitions |
blocks | No | Nested block definitions |
blocks_presentation | No | "list" (flat) or "tree" (nested hierarchy) |
presets | No | Default configurations for initial values |
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
| Section | Why Site-Scoped |
|---|---|
header/masthead | Site title/logo is the same on every page |
nav/main | Navigation links are shared across the site |
footer/footer_01 | Footer 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
- You write CSS with Liquid inside
{% style %}...{% endstyle %}in your section.liquidfile - OQO extracts the block, compiles it with the section's settings, and saves it as a separate CSS file
- 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
| Do | Do Not |
|---|---|
CSS variables (--section-*) | Animations or keyframes |
| Simple property rules | Complex hover effects |
| Settings-driven values only | Static styles that never change |
Use [data-section-wrapper][data-section-id="{{ section.id }}"] selector | Use 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).
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 }}">« 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 »</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 valueneq-- 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--columnblocks acceptlink_itemchildren. The admin UI only allows valid child types.root: false--link_itemcannot be added at the top level; it must be inside acolumn.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
| Aspect | A: Flat | B: Indented | C: Nested |
|---|---|---|---|
| Use case | Feature cards, stats | Nav with sublinks | Footer columns, tabs |
blocks_presentation | "list" (default) | "tree" | "tree" |
| Block types | 1 | 1 | 2+ |
| Parent-child | None | User controls via indent | Schema defines via accept |
| Nesting depth | Flat | Unlimited | Defined by schema |
| Template access | section.blocks | block.blocks (recursive) | block.blocks.type |
Block Schema Properties
| Property | Description |
|---|---|
type | Unique identifier for the block type |
name | Display name in admin UI |
accept | Array of child block types this block can contain |
root | If false, block cannot be added at root level (must have a parent) |
limit | Maximum number of this block type per section |
settings | Array 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>© {{ 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>
| Attribute | Purpose |
|---|---|
gz-section | Common class for all sections |
gz-{category}-{name} | Section-type class for CSS targeting |
data-section-wrapper | Marker for the editor and CSS scoping |
data-section-id | Unique instance ID for this section |
data-section-type | Section template path |
Parameters
| Parameter | Description |
|---|---|
| First argument | HTML element: 'div', 'section', 'nav', 'header', 'footer', etc. |
class | CSS classes to add |
id | HTML id attribute |
Any data-* | Custom data attributes |
Best Practices
-
Always use
section_wrapper-- it provides the data attributes needed for the editor, live preview, and CSS scoping. -
Use
{% style %}blocks for settings-driven CSS -- they enable instant hot-reload of color and spacing changes. -
Keep
{% style %}blocks lightweight -- only CSS variables and simple property rules. Put animations and complex styles in your theme stylesheet. -
Provide presets -- sections should look good out of the box. Define sensible initial values in
presets. -
Group settings with headers -- use
"type": "header"to organize related settings visually. -
Use
show_iffor conditional settings -- hide settings that are irrelevant based on other selections. -
Use
color_alphafor section colors -- always with thecustom_colorscheckbox pattern andcolor_alpha_to_rgbafilter. -
Use semantic CSS variable names -- prefix section variables with
--section-*to distinguish from--theme-*global variables.
Related Documentation
- Settings System -- Complete reference for all setting types
- Liquid Tags -- Tags like
{% section_wrapper %},{% paginate %}, and{% render %} - Liquid Objects -- Objects available in templates (
section,post,collection, etc.) - CSS Architecture -- Theme CSS system and compilation
- Live Preview -- How the editor preview works