theme.json is the theme’s manifest. It is parsed into a
ThemeManifest (crates/ringroad-renderer/src/theme/manifest.rs) at engine
startup. Node schemas are not declared here, they live in each node
file’s {% schema %} block, but the manifest carries everything else:
{
"name": "Default",
"version": "1.0.0",
"settings": [ … ],
"presets": { … },
"section_groups": { … }
}| Key | Type | Purpose |
|---|---|---|
name |
string | Human-readable theme name. |
version |
string | Semver-ish theme version. |
settings |
array | Global theme settings schema (colors, fonts, layout). Drives the editor’s global settings panel and {{ settings.* }} in templates. |
presets |
object | Default node trees per page key, what a fresh page looks like before the merchant edits it. |
section_groups |
object | Named containers rendered on every page (header, footer), each with its own preset. |
settings, global settings schema
Each entry is a setting definition, the same JSON shape used inside node
{% schema %} blocks. The type field picks the control and its config
struct. The default theme declares 14 settings (plus 3 header dividers):
{
"type": "color_palette",
"id": "colors",
"label": "Colors",
"default": {
"primary": "#0f0f0f",
"primary_content": "#ffffff",
"secondary": "#8b6f4e",
"accent": "#3a4a3a",
"base_100": "#fafaf9",
"base_200": "#f2f0ed",
"base_300": "#6b6360",
"base_content": "#171717",
"success": "#3b5e3b",
"warning": "#b07d2e",
"error": "#9b2c2c"
}
}The colors palette is special: its keys become {{ settings.colors.* }}
in templates and drive the design tokens the layout emits. The other global
settings in the default theme:
| id | type | Default | Notes |
|---|---|---|---|
heading_font |
select |
old_standard |
Also inter / georgia / helvetica / system |
body_font |
select |
inter |
Same option set |
font_size |
range |
16 px (14–20) |
Base font size |
heading_weight |
select |
400 |
300–600 |
container_width |
select |
4xl |
xs–4xl container scale (20–56rem) |
section_spacing |
range |
5 rem (2–10) |
Vertical padding per section |
container_gutter |
range |
1.5 rem (0.5–4) |
Horizontal page padding |
corner_radius |
select |
sharp |
sharp / moderate / rounded / pill |
social_instagram … social_tiktok |
url |
, | Footer social links |
html_head |
textarea |
"" |
Raw HTML injected into <head> |
The select/range/url/textarea/header types are all documented in
Setting types.
presets, default page trees
A preset is a complete PageConfig: node instances keyed by stable IDs
plus an order array. When a page has no saved merchant config, the engine
falls back to theme.presets[page_key].
"presets": {
"index": {
"order": ["hero-1", "ft-1", "cols-1", "testim-1", "nl-1"],
"sections": {
"hero-1": { "type": "hero", "settings": {}, "children": {}, "child_order": [] },
"ft-1": {
"type": "featured_products",
"settings": {},
"children": {
"ft-img": { "type": "product_image", "settings": {} },
"ft-title": { "type": "product_title", "settings": {} },
"ft-price": { "type": "product_price", "settings": { "show_compare": true } }
},
"child_order": ["ft-img", "ft-title", "ft-price"]
}
}
}
}The default theme defines presets for index, product, contact,
catalog, collection, search, and collections. cart and checkout
have empty presets (order: []) because those pages are hand-written
templates, not node compositions.
Pre-filled children
A preset can instantiate entire child trees with settings, note that the
merchant’s page config replaces the whole preset when saved, so presets
are only the starting point. featured_products’s children (product_image
→ product_title → product_price) demonstrate the “atomic product card”
pattern: a section node renders each child once per product (see
Children strategies).
section_groups, the shared chrome
Section groups are named node containers rendered on every page. The
renderer keys them by convention: @root:header-group and
@root:footer-group (constants in src/keys.rs). Each group has a preset
exactly like a page:
"section_groups": {
"@root:header-group": {
"name": "Header",
"max_sections": 3,
"preset": {
"order": ["announce-1", "navbar-1"],
"sections": {
"announce-1": { "type": "announcement_bar", "settings": {}, "children": {}, "child_order": [] },
"navbar-1": { "type": "navbar", "settings": {}, "children": { … }, "child_order": [ … ] }
}
}
},
"@root:footer-group": {
"name": "Footer",
"max_sections": 3,
"preset": { … }
}
}max_sections caps how many top-level nodes the editor lets a merchant
place in the group. allowed_sections (an optional allow-list of node
types) is supported by the schema but unused by the default theme.
Validation at load
The engine validates the manifest when the theme loads: every setting in
settings and in every node’s {% schema %} must pass
SettingDefinition::validate() (for example a range default must sit
between min and max, a select default must be one of its options).
A bad schema fails engine startup, never render time. See
Settings validation for the full rule list.