Nodes are the unit of editing in a theme. Every node is one file in
nodes/, and the filename is the type. This guide walks through the
anatomy, then the decisions you make when designing a node.
Anatomy of a node file
The default theme’s order is: body → {% stylesheet %} → {% javascript %}
→ {% schema %}.
1. The role marker (line 1)
@editor:section {# top-level: "Add section" panel, page or group #}
@editor:any {# nested: "Add child" panel inside another node #}The marker is extracted at load time into the schema’s role. hero,
navbar, footer, columns, featured_products are sections;
text_block, button, navlink, product_title, social_link are
content atoms. Sections may contain children (their schema says what
they accept); atoms are leaves.
2. The body, template logic against node
{% set align_class = "rr-hero--text-" ~ (node.settings.text_alignment or "center") %}
<div class="rr-hero {{ align_class }}">
{% if node.settings.heading %}<h1>{{ node.settings.heading }}</h1>{% endif %}
{{ node.slots.cta }}
</div>Guidelines from the default theme:
- Settings are always populated, defaults were merged before render.
Guard with
{% if %}only when the default is empty/meaningless. - Prefer
or "fallback"for values that may be unset (node.settings.text_alignment or "center"). - Escape user-entered text:
{{ node.settings.text | render_text | newlines_to_br }}for merchant text; plain{{ }}auto-escapes everything else.
3. The schema, what the editor shows
See Schema blocks for the full field reference. The decisions:
tag, pick the semantic element:sectionfor page sections,divfor generic containers,footer/header/article/asidewhere they fit. The renderer wraps your body in this tag withid="rr-node-{id}"andrr-node rr-node--{type}classes.limit, cap instances (hero: 1,newsletter: 2,featured_collections: 3).accepts, which children are allowed.["@theme"]admits any node (columns,rows,navbar); explicit lists give the editor a curated palette (footeraccepts onlyfooter_column+newsletter_column).slots, named buckets when children play different roles (hero’scolandctaslots).
4. Asset blocks
{% stylesheet %}
.rr-hero { … }
{% endstylesheet %}
{% javascript %}
const el = document.getElementById('$$node_id$$');
/* … */
{% endjavascript %}- CSS is subset per rendered type: include your own CSS here, and
it ships whenever the node renders. Use
var(--color-*)tokens, not hardcoded colors. - JS is emitted per node instance with
$$node_id$$replaced by the node’s DOM id. Wrap selectors in$$node_id$$scoping, the default theme does this inannouncement_bar,navbar,navmenu, andslideshow. - Not every node needs blocks. 24 of the default theme’s 45 nodes have a stylesheet; only 5 have JavaScript.
Section vs simple: what differs
Nothing at runtime, both render through the same recursive path. The role only changes where the editor lets the merchant add the node. The default theme’s split:
- Sections compose pages:
hero,columns,rows,slideshow,rich_text,featured_products,featured_collections,testimonials,newsletter,announcement_bar,navbar,footer,contact_form,catalog_products,collection_products,collections_list,search_results,product_detail,product_recommendations. - Simple nodes are atoms:
text_block,image,button,button_block,navlink,navmenu,navmenu_item,social_link,testimonial,collection_card,footer_column,newsletter_column,quantity_selector,variant_picker,buy_buttons, all theproduct_*atoms,product_accordion,product_share,product_metafields,product_gallery,product_badge,product_description,product_title,product_vendor,product_price,product_image,collection_filters(no schema).
Children strategies
A parent node has three ways to place its children. Pick deliberately:
1. render_node(child), inline, current scope
{% for child in node.children %}
{{ render_node(child) }}
{% endfor %}Children render now, with the parent’s loop scope visible. This is how
featured_products renders its product-card atoms once per product, and
how columns/rows lay out arbitrary children.
2. node.children_rendered, pre-rendered, simple
{{ node.children_rendered }}Children were rendered before the parent’s template ran (default slot
only). Simple, but loop scope from the parent isn’t visible to children.
Used by navbar (renders the nav twice, desktop + mobile, with one
render), footer, navmenu.
3. Manual dispatch, the orchestrator pattern
{% for child in node.children %}
{% if child.type == 'product_image' %}…{% elif child.type == 'product_title' %}…{% endif %}
{% endfor %}The parent decides per child what markup to emit. product_detail (the
PDP orchestrator) is the extreme case, 14 accepted child types, each
rendered into a specific column with specific wrapper markup; rich_text,
testimonials, featured_collections, newsletter, and slideshow
dispatch similarly.
Note:
featured_collectionsandslideshowinline their children’s markup rather than callingrender_node, the child schema exists for the editor, but the parent owns the output.slideshow’s preset uses a built-inslidechild type that has no node file at all.
Slots in practice
Declared slots group children by purpose:
"slots": {
"col": { "accepts": ["@theme"] },
"cta": { "accepts": ["button"], "max": 3 }
}{{ node.slots.col }}
{% if node.slots.cta %}<div class="rr-hero__actions">{{ node.slots.cta }}</div>{% endif %}Slot HTML is pre-rendered per slot name; children without a slot field
land in default (also available as node.children_rendered). The
editor’s “Add child” panel shows only the types each slot accepts.
Editor hooks in your markup
The renderer wraps every node, but children you emit inside a loop or
deep in your markup lose the wrapper’s data-rr-* attrs. Emit them
yourself when it matters:
<div {{ child.attributes }}>…</div>node.attributes expands to data-rr-node="{id}" data-rr-type="{type}"
- the preview editor’s selection overlay targets those.
Known default-theme quirks to avoid
button_block’s schema has nosizesetting, butrich_textpasseschild.settings.sizeto it, a latent inconsistency.collection_filtershas no{% schema %}, developer-managed only.- The
image_urlfilter used bynavbar/product_gallery/product_recommendationsis not registered in the engine (see Filters).