JavaScript Variants
When to use JavaScript variants instead of CSS, how they're injected, the safety considerations that matter, and worked examples for the tests CSS can't express.
JavaScript variants are how you express changes that CSS can’t — text content, conditional logic, DOM rewrites, event firing. They’re more powerful than CSS and correspondingly more dangerous, because a bug in your variant can break the page for the visitors assigned to it.
This guide covers when JS is the right tool, how Split Test Pro runs it, and the patterns that show up most often.
When JS Is the Right Tool
Reach for a JS variant when you need to:
- Change the text content of an element (copy A/B tests).
- Add or remove DOM nodes (insert a banner, remove a section).
- Apply conditional logic based on the visitor’s state (logged-in vs anonymous, cart contents, geo).
- Fire an analytics event when the variant becomes active (for downstream tools like GA4 or a data warehouse).
- Override an inline
style="..."attribute that even repeated!importantCSS can’t beat.
If your test is purely visual (color, size, hide/show), use a CSS variant — it’s faster, safer, and can’t break the page.
How JS Injection Works
When a visitor is assigned to a JS variant:
- The script creates a
<script>element containing your variant’s JS code. - It appends the element to the
<body>, which causes the browser to execute the code synchronously. - Your code runs in the page’s global JS context —
window,document, and any third-party globals (jQuery, Shopify object, dataLayer) are all available.
Inline vs external
Like CSS, you can paste your code inline or point at an external URL on your CDN:
- Inline — versioned with the experiment, easy to inspect. Best for self-contained variant logic.
- External URL — useful when the script is large, reused across experiments, or maintained by a developer separately.
Inline is the default; external is the exception.
Patterns You’ll Actually Use
Change text content
The single most common JS-variant use case:
const heading = document.querySelector(".product__title");
if (heading) {
heading.textContent = "Limited Edition — Only 50 Left";
}
Always null-check before manipulating. The element may not be present on every page, even within your targeting.
Wait for an element that loads later
Many themes render parts of the page asynchronously. If your target element doesn’t exist when the script runs, use a MutationObserver:
const observer = new MutationObserver(() => {
const cta = document.querySelector(".dynamic-cta");
if (cta) {
cta.textContent = "Get 20% Off Today";
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
This watches the DOM and modifies the element the moment it appears, then stops observing.
Insert a new element
const productInfo = document.querySelector(".product__info");
if (productInfo) {
const banner = document.createElement("div");
banner.className = "stp-trust-banner";
banner.textContent = "✓ Free shipping over $50";
banner.style.cssText = "padding:12px;background:#f3f4f6;border-radius:6px;margin:8px 0;";
productInfo.prepend(banner);
}
Inline styles or a class? Inline is simpler for one-off variants. If you’re adding several elements with shared styling, define a CSS class in the variant’s CSS field and reference it from JS — splitting the visual from the logic keeps each piece readable.
Remove an element
document.querySelectorAll(".announcement-bar, .promo-banner")
.forEach((el) => el.remove());
For pure removal, prefer a CSS variant with display: none !important — it’s safer and accomplishes the same thing visually. Use JS only when you need to also untrack analytics events fired by the removed element.
Conditional logic
Show a different price label depending on whether the visitor is in the EU:
fetch("https://your-geo-api.example.com/country")
.then((r) => r.json())
.then(({ country }) => {
if (["DE", "FR", "IT", "ES"].includes(country)) {
const label = document.querySelector(".price__label");
if (label) label.textContent = "Includes VAT";
}
})
.catch(() => { /* ignore — fall back to default */ });
Always include a .catch() (or try/catch around await) when fetching from an external API — a network failure shouldn’t break the page.
Fire an analytics event when the variant activates
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: "stp_variant_seen",
experiment: "product_page_orange_cta",
variant: "B",
});
Useful when you want to cross-reference Split Test Pro results with another analytics tool.
Override a stubborn inline style
When the theme sets style="..." on an element via JS at runtime, even !important CSS can lose. Strip the inline attribute or override it directly:
const cta = document.querySelector(".sticky-cta");
if (cta) {
cta.style.removeProperty("background-color");
cta.style.setProperty("background-color", "#f5620a", "important");
}
Safety Patterns
Always null-check selectors
Never assume your selector matches:
/* Bad — throws if .price isn't on the page */
document.querySelector(".price").textContent = "Free";
/* Good */
const price = document.querySelector(".price");
if (price) price.textContent = "Free";
Wrap risky logic in try/catch
If you’re calling third-party libraries or doing anything that could throw, wrap it:
try {
riskyThirdPartyCall();
} catch (e) {
/* Don't let it cascade */
console.warn("[stp variant] caught:", e);
}
Don’t block the main thread
A for loop over 50,000 items will freeze the page. If your variant needs heavy computation, defer it:
requestAnimationFrame(() => {
/* Heavy work here */
});
In practice, variant JS should be small and finish in under a millisecond. If your code is doing more than that, reconsider whether the test belongs as an A/B variant or as a permanent feature behind a flag.
Test in private/incognito
Cookies persist your variant assignment. Testing in a normal window means you keep seeing the same variant. Open an incognito window to get a fresh assignment, or use the preview cookie to force a specific variant.
When You Need Both CSS and JS
Many variants combine both: CSS for the styling, JS for the dynamic behavior. A common pattern is testing a new sticky add-to-cart bar:
- CSS field: the bar’s appearance (
position: sticky, color, padding). - JS field: create the bar, insert it into the DOM, hide it on scroll up, show on scroll down.
Each field is independent. Fill in both and they’ll both apply when a visitor is bucketed into the variant.
Next Steps
- For purely visual changes, prefer: CSS Variants.
- Test a redesign too different to express inline: Redirect Tests.
- Find robust selectors for your JS to target: Selector Cookbook.
Ready to start testing?
Install Split Test Pro and run your first experiment today.