Storefront markup
Last updated: May 16, 2026
Simple Split Testing is markup-driven. You add data attributes to elements in your theme; the embed script swaps them in or out per visitor. No JavaScript, no framework, no build step.
The two required attributes
Every element that takes part in a test needs both:
| Attribute | Value |
|---|---|
data-split-id | The test handle from the admin |
data-split-variant | The variant handle within that test |
Both are visible in the admin on the test detail page. Variant handles are usually control, variant-a, variant-b, and so on, but you can rename them.
Element swap
The default behaviour. Each element that matches a data-split-id is kept if the visitor is in the matching variant, and removed from the DOM if not.
<h1 data-split-id="hero-test"
data-split-variant="control">
Welcome to our store
</h1>
<h1 data-split-id="hero-test"
data-split-variant="variant-a">
Big sale on right now
</h1>
A visitor bucketed into control sees the first headline; the second is removed before the page is revealed. A visitor in variant-a sees the opposite. Elements with a data-split-id for a test that does not exist or is not yet active are left untouched.
Style swap
Apply variant-specific inline styles to the same element instead of duplicating markup.
<button data-split-id="cta-color"
data-split-variant="green"
data-split-styles="background: #2A8A4F; color: white;">
Buy now
</button>
When the visitor is assigned to green, the script sets the element’s style attribute to the data-split-styles value. Visitors in other variants are not assigned to this element, so it is removed.
To keep the same element across variants and only change the style, render one element per variant on the page and give each the same content:
<button data-split-id="cta-color"
data-split-variant="green"
data-split-styles="background: #2A8A4F; color: white;">
Buy now
</button>
<button data-split-id="cta-color"
data-split-variant="blue"
data-split-styles="background: #2563EB; color: white;">
Buy now
</button>
Class swap
Add or remove CSS classes when the visitor is in the assigned variant.
<button data-split-id="cta-color"
data-split-variant="blue"
data-split-add-classes="btn--blue large"
data-split-remove-classes="btn--default">
Buy now
</button>
data-split-add-classes- space-separated class names added to the elementdata-split-remove-classes- space-separated class names removed from the element
Useful when your theme already defines variant styles in CSS and you just want to toggle them.
Attribute swap
Vary any element attribute per variant using data-split-set-attr-<name>="<value>". When the visitor is assigned to that variant, the embed writes <name>="<value>" onto the element. This is how you A/B test things like CTA destinations, hero images, or aria-label text without duplicating the element.
<a data-split-id="hero-cta"
data-split-variant="treatment"
href="/collections/all"
data-split-set-attr-href="/collections/sale"
data-split-set-attr-aria-label="Shop the sale">
Shop
</a>
When treatment is assigned: href becomes /collections/sale, aria-label becomes Shop the sale. When control is assigned: this element is removed (standard element-swap behaviour).
Allowed target attributes
Anything outside this allow-list is dropped silently.
| Category | Attributes |
|---|---|
| Standard | href, src, srcset, sizes, alt, title, value, placeholder, poster, loading, decoding, width, height, target, rel, download, type, media, name |
| ARIA | any aria-* attribute |
| Data | any data-* attribute (except the reserved data-split-* namespace) |
Not allowed
These are dropped silently — use the alternative listed:
| Attribute | Use instead |
|---|---|
style | data-split-styles |
class | data-split-add-classes / data-split-remove-classes |
Event handlers (onclick, any on*) | Listen for window.splt.onAssignments() and bind in JS |
srcdoc, formaction, action, http-equiv | Out of scope; rewrite the variant element instead |
URL-bearing attributes (href, src, srcset, poster) additionally reject values containing javascript: or vbscript:.
You can combine attribute swap freely with data-split-styles, data-split-add-classes, and data-split-remove-classes on the same element.
Template-tag insertion
To avoid first-paint cost for the non-assigned branches, wrap the variant’s content in a <template> tag with the data attributes. The script unwraps the template only when the visitor is in the matching variant.
<template data-split-id="banner-test"
data-split-variant="promo">
<div class="promo-banner">Free shipping today!</div>
</template>
Browsers do not render <template> contents, so visitors in other variants pay nothing for the markup. Use this for hero swaps, banners, or any sizeable variant-only block.
Combining attributes
You can combine data-split-styles, data-split-add-classes, data-split-remove-classes, and any number of data-split-set-attr-<name> on the same element. All are applied when the variant matches.
<a data-split-id="hero-cta"
data-split-variant="bold"
data-split-add-classes="cta--bold"
data-split-styles="font-weight: 700; text-transform: uppercase;"
data-split-set-attr-href="/collections/sale"
href="/collections/all">
Shop the sale
</a>
Where you can put the markup
Anywhere in your theme:
- Inside sections (
sections/*.liquid) - Inside snippets (
snippets/*.liquid) - Inside theme-app-block schemas
- Directly in
layout/theme.liquid - Inside metafield-rendered content (as long as the data attributes survive your sanitiser)
The script applies assignments once when the DOM is ready, then re-applies on shopify:section:load and turbo:load so AJAX section updates and Hotwire-based themes pick up variants without a full reload. Late-injected elements outside those events are not handled automatically; if you inject markup yourself, dispatch one of those events on document to retrigger.
Listening for assignments in your own code
For most custom theme JavaScript — analytics tagging, conditional behaviour, anything that loads after the embed — use the window.splt global. It’s available synchronously from the moment the embed begins executing.
// Properties
window.splt.assignments // Array<{ splitTestId: string, variant: { id: string, handle: string } }>
window.splt.isPreview // true when ?splt_preview=<token> is set
window.splt.debug // true when ?splt_debug=true is set
// Get the variant handle the visitor is in for a test, or null:
const variant = window.splt.getVariant('hero-cta');
// Subscribe — fires once when assignments resolve. If they have already
// resolved by the time you subscribe, your callback fires synchronously
// with the cached payload. Safe to call from any late-loaded script.
window.splt.onAssignments((list) => {
list.forEach(({ splitTestId, variant }) => {
gtag('event', 'experiment_impression', {
experiment_id: splitTestId,
variant_id: variant.handle,
});
});
});
The variant payload is intentionally { id, handle } only. Names and descriptions stay server-side so storefront visitors cannot read merchant-facing test copy.
apply_assignments CustomEvent (still supported)
The original event-based API is still wired up. Use it when you need a hook that fires every time assignments re-apply (after the initial load, then on each shopify:section:load and turbo:load):
document.addEventListener('apply_assignments', (event) => {
const { assignments } = event.detail;
const heroTest = assignments.find((a) => a.splitTestId === 'hero-test');
if (heroTest?.variant?.handle === 'variant-a') {
// run variant-a-specific JS
}
});
The event does not fire if the assignment fetch fails — in that case the script falls back to keeping the first variant per data-split-id (the control) and removing the rest.
If you only need to read the current state once, prefer window.splt. The CustomEvent is the right primitive when you need to react to every re-application in SPA / turbo themes.
What the script will not do
- It will not load remote CSS or JS per variant. Stick to inline styles, classes, and DOM you already render.
- It will not run on the checkout. Checkout is governed by Shopify and not by your theme. Test checkout-affecting changes upstream of checkout (cart page, product page).
Fallback when the assignment fetch fails
If the assignment fetch errors out (network drop, 5xx, timeout), the script collapses to a control fallback: for each data-split-id, it keeps the first matching element on the page and removes the rest. Author your variants with the control element first so this fallback renders something sensible.
A 1.5-second safety timer also reveals the page if the script hangs for any reason, so a broken embed cannot keep the storefront blank.
Debugging
Append ?splt_debug=true to any storefront URL to enable verbose console logging from the embed. You’ll see the visitor’s assignments and how many elements were matched and applied — useful when your markup isn’t behaving the way you expect. The embed also logs a console.table() of the assignments once they resolve, which is the fastest way to scan a multi-test page.