Skip to content

Templates, layout, and snippets

The page shell, page templates, macro snippets, and the asset pipeline in practice.

Updated View as Markdown

Pages are composed from three layers: the layout shell (layout/theme.jinja), page templates (templates/{page_key}.jinja), and the node trees from presets/merchant config. Snippets are the shared macro vocabulary.

The layout shell, in practice

themes/default/layout/theme.jinja does five jobs:

  1. Fonts, a font_stacks map translates settings.heading_font / settings.body_font into CSS font stacks and Google Fonts URLs; webfont links are emitted only for the fonts that need them.
  2. Design tokens, an inline <style> emits :root custom properties from the global settings: --color-* (13 palette roles), --font-heading, --font-sans, --font-base-size, --font-heading-weight, --radius-{sm,md,lg,xl} (from a radius_map), --spacing-section, --spacing-container, --container-max. This is how merchants’ global settings restyle the whole theme without a rebuild.

The theme’s responsive system is a single breakpoint ladder, used mobile-first everywhere (min-width, expressed as rem literals in media queries):

Step Viewport Step Viewport
xs 320px (20rem) 2xl 672px (42rem)
sm 384px (24rem) 3xl 768px (48rem)
md 448px (28rem) 4xl 896px (56rem)
lg 512px (32rem)
xl 576px (36rem)

The same scale is exposed as --container-{xs…4xl} tokens (also the steps for the container_width global setting and any inline max-width a template needs). Because CSS custom properties can’t be used inside media queries, queries use the literal rem values — keep them aligned with the ladder. Layout is fluid by default: .container-page is width: 100% up to var(--container-max), grids reflow at their ladder step (rr-grid-2 ≥ 42rem, rr-grid-3 ≥ 48rem, rr-grid-4 ≥ 56rem), and mobile-only states live below 48rem (3xl) unless a step says otherwise. 3. Slots, {{ settings.html_head | safe }} and {{ page_styles | safe }} in <head>; {{ header_html | safe }}, {% block main %}{{ page_content | safe }}{% endblock %}, {{ footer_html | safe }}, and {{ page_scripts | safe }} in <body>. 4. Global assets, / _assets/main.css and / _assets/main.js (deferred). 5. Shared chrome, the skip link and the cart drawer ({% include "default/snippets/layout/cart_drawer.jinja" ignore missing %}).

Themes must keep the token emission and the five slots; everything else is yours to change.

Page templates: stub vs real

The default theme uses two patterns:

Stub templates (section-composed pages)

{% extends "default/layout/theme.jinja" %}
{% block main %}{{ page_content | safe }}{% endblock %}

index, product, collection, catalog, search, collections, contact are all 102-byte stubs, the page key’s preset (or the merchant’s saved config) provides the node tree, and page_content is its rendered HTML. Adding a new page type = adding a preset in theme.json + a stub template, unless the page needs bespoke markup.

Real templates (bespoke pages)

cart.jinja, checkout.jinja, 404.jinja, and the account/* family override {% block main %} with hand-written markup:

  • cart.jinja loops cart.items (image, title link, price | money, line totals, cart.total_price | money), renders a qty_stepper and remove buttons per line, a coupon form, and a checkout form, with a full empty-state branch.
  • checkout.jinja branches on order_id, confirmation page vs the full checkout form with a client-filled order summary.
  • account/* render shells whose dynamic parts are filled client-side by main.js (data-rr-orders-list, data-rr-order-*).

Real templates may carry their own {% stylesheet %} blocks (404, account pages), those join page_styles.

Snippets: the macro vocabulary

Snippets are Jinja macros imported by namespaced path. The default theme’s ui/ set is the shared vocabulary, copy it into your theme:

Macro Signature Renders
icon icon(name, size="md", class="") 28 inline SVGs (cart, search, chevrons, socials, …)
button button(label, variant, size, type, href, …) <a> or <button> with rr-btn rr-btn--{variant} rr-btn--{size}
input input(name, label, type, placeholder, value, …) label + styled input + error/help
textarea textarea(name, label, …) label + styled textarea
qty_stepper qty_stepper(value, min, max, name, …) − / + stepper driven by main.js
price price(amount, currency, compare_at, …) money-formatted price with sale state
badge badge(label, tone, size) rr-badge rr-badge--{tone}
empty_state empty_state(title, message, icon_name, cta_label, cta_href) empty listings
section_heading section_heading(title, subtitle, href, link_label, align) h2 + subtitle + view-all link
breadcrumbs breadcrumbs(items) {label, href} list with chevrons
pagination pagination(pg, base) prev/page/next links
listing_toolbar listing_toolbar(total, sort, action, collection_slug) count + sort select

Plus product/ (product_card, product_grid, add_to_cart) and layout/ (search_bar macro, cart_drawer include).

Import them where needed, imports are cheap and the engine caches templates:

{% from "default/snippets/ui/button.jinja" import button %}
{{ button(label="Proceed to checkout", variant="primary", size="lg", type="submit") }}

The asset pipeline, in practice

  • Global, assets/main.css / main.js, served at /_assets/*. The default theme’s CSS is 100% token-driven (all styling through var(--color-*) etc.); its JS is a dependency-free IIFE that drives every interaction from data-* attributes (cart store + drawer, quick-add, PDP add-to-cart, variant picker, gallery, qty steppers, announcement dismiss).
  • Per-node, {% stylesheet %} / {% javascript %} blocks, subset/injected per page (see Asset blocks).
  • Build artifacts, ringroad-renderer build emits combined.{hash}.css / .js for CDN caching via Engine::build_assets.

Division of labor: behavior that spans the whole theme (cart, drawer, forms) belongs in main.js keyed by data-*; behavior that’s specific to one node type belongs in its {% javascript %} block scoped with $$node_id$$.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close