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:
- Fonts, a
font_stacksmap translatessettings.heading_font/settings.body_fontinto CSS font stacks and Google Fonts URLs; webfont links are emitted only for the fonts that need them. - Design tokens, an inline
<style>emits:rootcustom 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 aradius_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.jinjaloopscart.items(image, title link,price | money, line totals,cart.total_price | money), renders aqty_stepperand remove buttons per line, a coupon form, and a checkout form, with a full empty-state branch.checkout.jinjabranches onorder_id, confirmation page vs the full checkout form with a client-filled order summary.account/*render shells whose dynamic parts are filled client-side bymain.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 throughvar(--color-*)etc.); its JS is a dependency-free IIFE that drives every interaction fromdata-*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 buildemitscombined.{hash}.css/.jsfor CDN caching viaEngine::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$$.