Assets, motion and effects
The platform serves what a theme ships, bundles what its sections declare, and supplies the runtimes a theme should not write: motion, effects and behaviour.
asset_url and versioned assets
{{ 'theme.css' | asset_url }} gives a file in assets/ its content-versioned address — /theme-assets/{version}/{sha}/assets/theme.css — cacheable for a year and immutable, because the address changes with the bytes. A name the theme does not have is a check error (asset.missing) and renders as nothing. Pictures a theme ships (its sample photographs, its section thumbnails) are addressed the same way through image_picker values of the form { "asset": "images/hero.webp" }.
Rules the check applies to what assets/ holds:
- A script from another site (
<script src="https://…">) is an error: package it inassets/. So is an@importin CSS and aurl(http:…)or protocol-relativeurl(//…); anhttps:URL in CSS is a warning to review as a dependency. - Published sites run no inline script. An inline
<script>(other thantype="application/json"orapplication/ld+json) and an inline event handler (onclick=…) are errors. Attach listeners from the theme's script. - A theme's own script is served only on the site's own origin and only for a theme the platform trusts — its own themes and reviewed marketplace themes. On the platform path, on a developer's theme, in the editor and in the preview,
script_tagrenders nothing. Every page must work without the theme's script. - An SVG may carry no script, no
on…handler, nojavascript:link, no<foreignObject>,<iframe>,<embed>or<object>, and no external document reference. - A picture whose bytes are not the image its name says is an error.
Bundles
Every section's and block's {% stylesheet %} is concatenated (in file order, each preceded by a comment naming its file) into one stylesheet the head links at /theme-assets/{version}/bundle/{hash}.css. Every {% javascript %} is concatenated into one script, each file's code wrapped in its own try { … } catch so top-level names never collide and one failing never stops the rest, served at …/bundle/{hash}.js and only where scripts are allowed. Both are cached by content hash: a published version's hash never changes, a draft's changes with every edit.
Fonts
Fonts are the platform's, listed on Settings: SIL Open Font License families served from the platform's origin under /fonts/themes/, one variable latin file per style, the upright file preloaded, font-display: swap. A site's page keeps font-src 'self', so a theme cannot load type from a font service — a @font-face pointing off-origin is refused by the page's policy even if the check let it through. A theme may ship its own .woff2 in assets/ (it counts against the budget and the asset count).
Motion
An animation setting holds an entrance and its timing; the theme writes {{ section.settings.entrance | animation_attributes }} on the element; the platform's abizvo-motion.js plays it with an IntersectionObserver and CSS transitions of opacity, transform and clip-path only. The script is sent to a page when any rendered element asked for an entrance, or when the manifest declares features.animations.
- Nothing is hidden until the script runs. The start state (
opacity: 0, the transform, the clip) is applied only underhtml.ab-motion, which the runtime adds — so a crawler, a visitor without script and the editor canvas see every element as it is. - Reduced motion wins. Under
prefers-reduced-motion: reducethe runtime does nothing. - A clip reveal is watched through its parent and decided by its own box.
reveal,mask-reveal,image-revealandtext-revealstart asclip-path: inset(0 0 100% 0), and an observer measures a target after its own clip — a fully clipped element never intersects and would stay hidden for good. The runtime observes the parent, which is not clipped, and uses the element's own box (which a clip does not change) to decide when to play. Marigold's hero stayed invisible until this rule existed. - An absolutely positioned element has no height in flow. An observer can never see it intersect, so it waits at opacity 0 for a reveal that cannot arrive — and its tail runs past the foot of its own section. Give the animated element a box in flow, or animate its container. (Quire's marginal note,
QuireThemeTests.A_note_beside_the_column_reveals_from_the_element_that_has_height.) - The
children: trueoption staggers the element's children bystaggermilliseconds;trigger: scrolltiesparallaxandsticky-revealto the scroll position, read once per frame.
Effects
An effect setting picks one of eleven interactions; the theme writes {{ section.settings.effect | effect_attributes }} — data-ab-fx="…" and data-ab-fx-k="…" — and the platform sends its effects stylesheet and abizvo-effects.js only to a page that uses one. Owners choose effects; nobody but the platform writes the code that runs them.
| Effect | What it does |
|---|---|
3D tilt (tilt) |
Leans toward the pointer in three dimensions |
Magnetic (magnetic) |
Drifts toward the pointer — for buttons and small cards |
Spotlight (spotlight) |
A soft light follows the pointer across the surface |
Lift (lift) |
Rises with a deeper shadow under the pointer |
Image zoom (zoom) |
Pictures inside zoom gently within their frame |
Zoom on scroll (scroll-zoom) |
Pictures settle from a slight zoom as they pass through the screen |
Count up (counter) |
Numbers count up from zero when they come into view |
Stacking cards (sticky-stack) |
The items stick and stack on top of each other as the page scrolls |
Marquee (marquee) |
The items glide sideways in an endless row; hovering pauses it |
Living gradient (gradient) |
A slow, soft gradient drifts behind the content |
Word by word (split-words) |
Headlines rise into place one word at a time |
Rules the runtime keeps, which a theme relies on rather than restates: nothing hides content; pointer effects apply only under (hover: hover) and (pointer: fine); everything that moves sits under prefers-reduced-motion: no-preference; a marquee's copy is aria-hidden and never focusable; word-by-word never splits text the editor lets the owner type in place. The intensity is a keyword the stylesheet turns into --ab-fx-k (0.5, 1, 1.6), and spotlight and gradient set position: relative; isolation: isolate on the element.
The behaviour runtime
Some storefront behaviour genuinely needs a script: a clock that ticks, buttons that move a rail, suggestions under a search box. A theme does not write that script. It writes plain markup with the attributes below, the page works without any script at all, and the platform links its own abizvo-behaviours.js — reviewed platform code, served at a content-versioned /site-assets/{hash}/js/abizvo-behaviours.js and cached for a year — only to a page whose finished markup carries one of these attributes, and never on the editor canvas. A paid theme keeps its zero bytes of JavaScript and still gets the behaviour.
Three rules hold for every behaviour, and a theme relies on them rather than restates them:
- Progressive. A control that only means something with the script (previous/next, pause, play) is written with the
hiddenattribute; the runtime shows it. An element the runtime has taken over carriesdata-ab-live, so a theme can style the two states ([data-ab-rail]:not([data-ab-live])is the no-script layout). - No movement without a way to stop it. Autoplay starts only where the theme wrote a pause button for it, never for a visitor who prefers reduced motion, and waits while the pointer or focus is inside or the tab is in the background. A pause button takes
aria-labelfor "Pause" anddata-ab-label-playfor "Play"; the runtime swaps them and setsaria-pressed. - Keyboard first. Everything reachable by pointer is reachable by keyboard, Escape closes what opened, and focus goes back where it came from.
| Attribute | On | What the runtime does | Without the script |
|---|---|---|---|
data-ab-countdown="{ISO date}" |
a wrapper holding <time data-ab-countdown-static>, a hidden [data-ab-countdown-clock] with [data-ab-countdown-unit="days\|hours\|minutes\|seconds"] children, and optionally a hidden [data-ab-countdown-done] |
Fills the units every second (or writes 3d 04:12:09 into the clock when it has no units), shows the clock by sight while the static date stays for screen readers, and at zero sets data-ab-ended and shows the done message |
The static date |
data-ab-rail |
a wrapper around a scroll-snap row ([data-ab-rail-track], or the wrapper itself) with hidden [data-ab-rail-prev] / [data-ab-rail-next] buttons |
Previous and next scroll by one item, disabled at either end. data-ab-rail-autoplay="{seconds}" advances (and wraps) on that interval when a hidden [data-ab-rail-pause] button is present |
A row that scrolls by swipe, wheel and keyboard |
data-ab-announcements="{seconds}" |
a wrapper of [data-ab-announcement] messages, with optional hidden [data-ab-announcements-prev], [data-ab-announcements-next] and [data-ab-announcements-pause] buttons |
Shows one message at a time; rotates on the interval only with a pause button | Every message shows |
data-ab-hide-on-scroll |
a sticky header | Steps aside (data-ab-away, the platform's translateY(-100%)) while the page scrolls down past it, returns on the way up and whenever something in it has focus; never moves under reduced motion |
A header that stays |
data-ab-video-toggle |
a hidden button with aria-controls="{video id}" |
Plays and pauses that video, keeping aria-pressed and the label in step; pauses an autoplaying video at once for a visitor who prefers reduced motion |
The video as its attributes say |
data-ab-predictive-search="{{ routes.predictive_search_url }}" |
a <form action="{{ routes.search_url }}" method="get" role="search"> holding input[name=q] and a hidden [data-ab-predictive-results] panel |
After 250 ms and two characters, fetches /search/suggest?q=… — the theme's sections/predictive-search.liquid, or the JSON fallback drawn as a list — into the panel; the input becomes a combobox (aria-expanded, aria-controls); ArrowDown moves into the results, ArrowUp back, Escape closes |
The form submits to the search page |
data-ab-auto-submit |
a GET form (the filter and sort form of a collection or the search page) | A changed box, select or field submits the form; buttons marked data-ab-auto-submit-hide step aside; on the page that comes back the control that changed has focus again, its <details> open |
The Apply button |
data-ab-popup="{key}" |
a hidden role="dialog" element or a <dialog>, with [data-ab-popup-close] buttons; data-ab-popup-delay="{seconds}", data-ab-popup-days="{days}" (30 by default) |
Shows it after the delay at most once per visitor per that many days (remembered in the browser), keeps focus inside, closes on Escape or a close button and gives focus back; a browser that cannot remember never shows it | Nothing interrupts the page |
data-ab-dialog="#{close id}" (alias data-ab-drawer) |
a :target drawer or dialog with an id, opened by a link to #{id} and closed by a link to the close address |
While it is the target, keeps Tab inside it and focuses its first control; Escape goes back when a link on this page opened it, else to the close address; focus returns to the link that opened it | Opens and closes by its links, as CSS :target |
The platform adds two rules of its own with the script: the header's slide, and the countdown's static date kept for screen readers. Everything else is the theme's CSS. The script is linked once whatever the page asks for, placed before </body>; app embeds may use the attributes too.
A theme that ships its own JavaScript is still held to the check's rules above, and a paid theme to its zero bytes — the runtime is not a theme file and nothing about it is configurable from one.
The performance budget
The check measures every theme and reports the figures whatever the verdict. The defaults are the published guidance; a deployment may tighten them.
| Measure | Warns above | Refuses above |
|---|---|---|
CSS (assets plus every {% stylesheet %}) |
150 KB | — |
JavaScript (assets plus every {% javascript %}) |
150 KB | 400 KB |
| One script file | — | 512 KB |
| One image | 800 KB | — |
| The whole package | 25 MB | 60 MB (a package cap) |
| Assets shipped | 60 | — |
A paid theme is held tighter than the budget — under 60 KB of CSS, no script, every sample photograph at most 250,000 bytes, the package under 5 MB. See Premium theme standards.
Pictures the platform serves
A library picture reaches a page through image_tag, which writes src, a srcset of the library's sm (480w), md (1024w) and lg (1920w) copies, sizes, the intrinsic width and height so nothing shifts as it loads, loading="lazy" unless told eager, decoding="async", and the focal point as object-position. Write sizes: honestly for the slot the picture fills, and mark the one picture above the fold loading: 'eager', fetchpriority: 'high'.