Split Test Pro
Advanced 8 min read

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 !important CSS 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:

  1. The script creates a <script> element containing your variant’s JS code.
  2. It appends the element to the <body>, which causes the browser to execute the code synchronously.
  3. 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

Ready to start testing?

Install Split Test Pro and run your first experiment today.

Install on Shopify