Developer reference
Aurelia is plain Liquid, plain CSS, and plain JavaScript. There is no build step,
no bundler, no framework, and no dependency to install. shopify theme dev
serves the files exactly as they sit on disk.
Layout of the theme
assets/ critical.css + one CSS/JS file per section and component
blocks/ group, text, custom-liquid — nestable inside Custom section
config/ settings_schema.json (theme settings), settings_data.json (the three styles)
docs/ this documentation; excluded from theme push by .shopifyignore
layout/ theme.liquid, password.liquid
locales/ en.default.json (storefront), en.default.schema.json (editor labels)
sections/ 27 addable sections + 25 template sections + the two section groups
snippets/ product-card, breadcrumbs, structured-data, meta-tags, wishlist, …
templates/ JSON templates, plus gift_card.liquid
How CSS and JS load
Aurelia deliberately does not use {% stylesheet %} and {% javascript %}.
Shopify concatenates those tags across the whole theme into a single bundle that
is served on every page — so a rule written for the ring sizer would ship on the
checkout-less 404 page too. Instead:
assets/critical.cssloads in<head>on every page. It holds the reset, the type scale, the button and form styles, and anything shared by three or more sections — including a handful of rules (.option-pill,.unit-price,.policy-links) whose markup is rendered by more than one section.- Each section loads only its own stylesheet, at the top of its own file:
liquid {{ 'section-hero.css' | asset_url | stylesheet_tag }} - Each section with behaviour loads its own script at the bottom:
```liquid
```
A page therefore carries the CSS and JS of the sections actually on it and
nothing else. The homepage lands around 93 KB of CSS and JS, the product page
around 98 KB, the collection page around 80 KB — each figure including the shared
28 KB of critical.css.
Two rules when you add a section
- Guard your script. A section can appear more than once on a page, and a
duplicate
<script src>executes again (a duplicate<link>does not). Every JS asset opens with a run-once flag:js if (!window.__aureliaSectionHero) { window.__aureliaSectionHero = true; // … } - Never put Liquid in an asset file.
assets/*.jsandassets/*.cssare static — Liquid is not evaluated there, so{{ 'foo' | t }}ships as literal text. Pass translated strings and merchant settings throughdata-*attributes on the markup instead, the waysnippets/wishlist.liquidcarries its announcement strings on a hidden<span>.
Colour
Three tokens are set in snippets/css-variables.liquid and everything else is
derived from them:
| Token | Source |
|---|---|
--color-background |
theme setting, or the section's colour scheme |
--color-foreground |
theme setting, or the section's colour scheme |
--color-accent |
theme setting, or the section's colour scheme |
--color-muted |
color-mix(…foreground 70%, background) |
--color-border |
color-mix(…foreground 14%, background) |
--color-on-media |
fixed #FDFCFA, for text over images |
--color-muted carries real body text — placeholders, unit prices, policy links,
captions — so the 70% mix is a contrast floor, not a taste decision. Below roughly
68% it drops under 4.5:1 on the warmer palettes. If you add a palette, check the
muted colour against its background before shipping it.
Section colour schemes come from settings_data.json. Each of the three presets
(Ivory, Noir, Champagne) defines its own color_schemes block, so switching
style changes the schemes too rather than leaving a dark band from the previous
palette.
Shared JavaScript
Five globals, all reachable on every page because the layout renders their
sections and snippets outside content_for_layout:
| Global | Defined in | Does |
|---|---|---|
AureliaAnnounce(text) |
snippets/live-region.liquid |
Speaks a message through the page's role="status" region — cart changes, wishlist saves, filter results |
AureliaCart |
assets/section-cart-drawer.js |
open, add, change, refresh; every add-to-cart on the storefront goes through it |
AureliaWishlist |
assets/component-wishlist.js |
toggle, all, sync; persists to localStorage, keeps aria-pressed in step |
AureliaMotion |
assets/motion-toggle.js |
Pause/play for anything that animates on its own; fires aurelia:motion |
AureliaQuickView |
assets/component-quick-view-modal.js |
Fetches a product form through the Section Rendering API and opens it in a <dialog> |
Section Rendering API
Two places fetch markup rather than rendering it inline:
- The cart drawer refreshes itself with
?sections=cart-drawer. Note that a<script>inserted viainnerHTMLdoes not execute — the drawer's behaviour is bound by delegation for exactly this reason. A<link>inserted the same way does load, which is why the refreshed markup can carry its own stylesheet tag. - Quick view fetches
?section_id=quick-view.
Accessibility conventions
- Dialogs are native
<dialog>elements. Focus trapping,Escape, and the backdrop are the browser's job, not ours. - Anything that changes without a page load announces through
AureliaAnnounce(WCAG 2.1 SC 4.1.3). - Toggles that turn something on and off — the wishlist heart — use
aria-pressedwith a state-neutral label ("Save to wishlist"), so a screen reader reads "Save to wishlist, pressed". Controls that switch between two different actions — the motion pause button — swap their accessible name instead. - Any motion that starts on its own and runs past five seconds renders
snippets/motion-toggle.liquidinside adata-motion-rootcontainer (SC 2.2.2).
Checks before you commit
shopify theme check
.theme-check.yml runs theme-check:all, not the recommended subset. The one
documented exception is AssetSizeJavaScript on templates/gift_card.liquid:
qr-code.js is 14 KB uncompressed but 4.5 KB over the wire, it is the theme's
own generator rather than a vendored library, and it loads on the gift-card page
alone. Every other template is held to the rule. A clean run reports zero
offenses — keep it that way rather than widening the ignore list.