Variation Code
A variation is one version of your experiment that a visitor can see: the original page (control) or a challenger. Each variation can have its own CSS and JavaScript. This page explains when that code runs and how the initVariation wrapper works. Following it helps you write variations that are reliable, fast, and safe to remove.
CSS injection
When a visitor is assigned to a variation, its CSS is injected first. A <style> tag containing all the variation's CSS rules is appended to the document's <head> element. This happens synchronously during experiment evaluation.
The page is still hidden by anti-flicker (a short delay that hides your page until the right variation is ready) at this point. Visitors never see the original version flash before it changes. See Anti-Flicker for the full detail.
Because the CSS lives in <head>, it applies to the whole page right away. Your CSS selectors style every matching element before the visitor ever sees the page.
/* This CSS is injected into <head> for visitors in this variation */.hero-title { font-size: 3rem; color: #1d4ed8;}.cta-button { background-color: #22c55e; border-radius: 9999px; padding: 1rem 2rem;}CSS injected by A vs B overrides your page's existing styles because it appears later in the document than your stylesheet links. If your existing styles use !important and you cannot override them, you may need to add !important to your variation CSS as well.
JavaScript injection: the initVariation wrapper
After CSS, the variation's JavaScript runs. Your code must be wrapped in a function named initVariation that receives a single options argument. This is exactly what a new variation starts with:
function initVariation(options: AvsbVariationOptions) { // Apply your variation changes here. This runs once the experiment activates. options.onRemove(function() { // Undo anything you added. Timers/listeners/waitUntil started through // options.* are cleaned up automatically. });}/** @type {(options: AvsbVariationOptions) => void} */function initVariation(options) { // Apply your variation changes here. This runs once the experiment activates. options.onRemove(function() { // Undo anything you added. Timers/listeners/waitUntil started through // options.* are cleaned up automatically. });}The snippet calls initVariation(options) automatically. The editor blocks saving if the wrapper function is missing or renamed.
The /** @type {...} */ comment above the variation.js example is optional. It is an ordinary JavaScript comment, so it is safe to paste anywhere and never runs. Adding it is what gives the code editor autocomplete on options, even in a plain JavaScript file.
When initVariation runs:
- The variation's CSS is already applied (CSS is injected first).
- The browser has almost always finished parsing the page's HTML by now. Before your code runs, A vs B already fetched the datafile (the small file listing every live experiment for your project). That trip takes real time. This is not a guarantee for every page, so still reach for
options.utils.waitForElement(below) if you are not sure an element exists yet. - The
window.avsbglobal API is available. - The page is still hidden by anti-flicker.
The options object
The options argument provides experiment context, lifecycle hooks, self-cleaning helpers, tracking utilities, and a recommendations client: everything your variation needs without reaching for global state.
Context properties
| Name | Type | Description |
|---|---|---|
options.experimentId | string | A vs B's internal id for the experiment (the UUID row in the Experiment details panel), not the numeric Experiment ID. |
options.experimentName | string | The display name of the experiment. |
options.variationId | string | A vs B's internal id for the variation assigned to this visitor. To address an experiment or variation from code, use the numeric ids that avsb.getVariation() and avsb.forceVariation() take. |
options.variationName | string | The display name of the variation (e.g. "Control", "Variant 1"). |
options.variationType | 'control' | 'variant' | Whether this visitor was assigned the control or a variant (any version that is not the control). |
Lifecycle hooks
| Name | Type | Description |
|---|---|---|
options.onActivation | (callback: () => void) => void | Register a callback to run when the variation activates. In variation code it fires as soon as your function returns, since the variation is already being applied by then. It exists mainly so the same options shape also works for trigger code, which gates activation on a condition you check yourself. |
options.onRemove | (callback: () => void) => void | Register a callback to run when the variation is removed: on SPA navigation, experiment teardown, or when the visitor calls avsb.disable(). Use this to undo DOM changes that the self-cleaning helpers cannot handle automatically. |
Self-cleaning helpers
These helpers work exactly like their native browser equivalents, but anything you start through them is automatically torn down when the variation is removed. You never need to cancel them manually.
| Name | Type | Description |
|---|---|---|
options.waitUntil | (target, opts?) => guarded promise | Waits for a CSS selector, a window.x.y global path, or a predicate function, then resolves with whatever the target matched. Automatically cancelled on variation removal. .catch() is optional: a wait that times out is logged, never thrown. Default timeout 10000ms. |
options.setTimeout | (fn: () => void, ms?: number) => handle | Like window.setTimeout, but the timer is automatically cleared when the variation is removed. |
options.setInterval | (fn: () => void, ms?: number) => handle | Like window.setInterval, but the interval is automatically cleared when the variation is removed. |
options.addEventListener | (target, type, handler, opts?) => void | Like target.addEventListener, but the listener is automatically removed when the variation is removed. |
options.utils | AvsbUtils | The full helper toolkit: waitForElement, onMutation, $, $$, createElement, cookie, storage, state, and more. Anything it starts is torn down with the variation, exactly like the helpers above. |
Tracking utilities
| Name | Type | Description |
|---|---|---|
options.track.event | (eventKey: string | number, properties?: { revenue?: number; value?: number }) => void | Manually fire a custom metric event for this visitor. The eventKey must match a metric configured in your project. |
options.track.segment | (segmentKey: string, segmentValue: string) => void | Send a segment value for this visitor. |
options.track.purchase | (order: PurchaseOrder) => void | Send a completed order immediately, for revenue metrics. orderId and total are required; currency, items, and a few other fields are optional. Not batched: this goes out right away rather than waiting for the usual send cycle. |
options.track.cart | (cart: { total?: number; totalMinor?: number }) => void | Tell A vs B the visitor's current cart total. This feeds audiences (named, reusable visitor groups) that target visitors by cart value. totalMinor (an integer in the smallest currency unit, e.g. cents) wins when both are set. Nothing is sent over the network for this one. |
options.getVisitorId | () => string | null | Returns the current visitor's unique identifier, or null when there is no id yet (consent mode before avsb.init(), or a visitor who denied analytics). In practice this is always a real id inside initVariation, since variation code only runs once a variation has already been applied to a visitor. |
options.getVariation | (experimentId: string | number) => string | null | Takes the numeric Experiment ID from the experiment details panel and returns the numeric Variation ID this visitor is assigned, or null if not assigned. |
options.getActiveExperiments | () => Array<{ experimentId: string; variationId: string }> | Returns all experiments the current visitor is currently active in, using those same numeric ids. |
options.forceVariation | (experimentId: string | number, variationId: string | number, opts?: { track?: boolean }) => boolean | Forces the current visitor into a specific variation of another experiment, by its numeric ids. Returns true if the variation was applied successfully. |
Recommendations
options.recs is a ready-to-use recommendations client, pre-bound to the running experiment and variation. Calling options.recs.get({ recipe }) resolves items for a recipe and automatically attributes clicks and impressions to this variation, with no extra setup. See the Recommendations API reference for the full method list.
Basic example
A simple variation that changes a heading and reverts it when the variation is removed (for example, on SPA navigation). initVariation runs only once the variation is already being applied. So your DOM changes go directly in the function body: there is no separate activation step to wait for.
/** @type {(options: AvsbVariationOptions) => void} */function initVariation(options) { // The function body IS the activation moment: apply changes directly const heading = document.querySelector('.hero h1'); if (heading instanceof HTMLElement) { heading.dataset.originalText = heading.textContent ?? ''; heading.textContent = 'The faster way to grow your business'; } // Revert DOM changes when the variation is removed (e.g. on SPA navigation) options.onRemove(() => { const original = document.querySelector('.hero h1'); if (original instanceof HTMLElement && original.dataset.originalText) { original.textContent = original.dataset.originalText; } });}Tracking from a variation
Fire your own events when you need to be precise about what counts as a conversion, instead of relying only on click and pageview metrics:
/** @type {(options: AvsbVariationOptions) => void} */function initVariation(options) { const upsellButton = document.querySelector('.accept-upsell'); if (upsellButton) { options.addEventListener(upsellButton, 'click', () => { // 'upsell_accepted' must match a metric's event key in this project options.track.event('upsell_accepted'); options.track.purchase({ orderId: 'ord_' + Date.now(), total: 19.99, currency: 'USD', }); }); }}Waiting for dynamic content
On single-page applications, some elements are rendered by the JavaScript framework after the initial DOM parse. Use options.utils.waitForElement to delay your DOM work until the target element is ready: it resolves with the element itself. For a condition that is not an element, a global variable, a value from another script, use options.waitUntil instead. Both run through options, so both are cancelled automatically if the variation is torn down while waiting.
/** @type {(options: AvsbVariationOptions) => void} */function initVariation(options) { // Give up after 5 seconds instead of the default 10 options.utils.waitForElement('#intercom-container', { timeout: 5000 }).then((container) => { // Safe to modify: the widget is ready container.classList.add('variant-position'); // Revert on removal options.onRemove(() => { const el = document.querySelector('#intercom-container'); if (el) el.classList.remove('variant-position'); }); });}Accessing the DOM
Variation JavaScript normally runs after the DOM is parsed (see above). So you can usually use standard DOM APIs, like document.querySelector and document.getElementById, directly in the initVariation function body. You do not need to wait for a DOMContentLoaded event.
On single-page applications, elements rendered by the framework after the initial page load may not be present yet. Use triggers to delay activation until the element exists, or wait for it directly inside initVariation with options.utils.waitForElement.
If your code throws
A synchronous error in initVariation, one that throws before the function returns, is caught. A vs B reports it (see error capture) and it does not stop the rest of the page. Your code did stop at the point of the error though, so any changes you planned after that line never happen. Write defensive code anyway:
/** @type {(options: AvsbVariationOptions) => void} */function initVariation(options) { // Good: check that the element exists before modifying it const button = document.querySelector('.cta-button'); if (button) { button.textContent = 'Start free trial'; } // Bad: throws an error if .cta-button doesn't exist // document.querySelector('.cta-button').textContent = 'Start free trial';}An error inside a .then(), a timer, or an event handler, anything that runs after initVariation has already returned, is not caught by that same wrapper. Project JavaScript drops this kind of error silently. Variation code does not: every script A vs B injects carries a hidden marker naming its exact experiment and variation. So even a later, asynchronous error is traced back to the code that caused it.
The AI assistant can add JavaScript through the visual editor. That code runs through the same steps this page describes, and its errors are caught and reported the same way too. There is no separate, less-checked path for AI-written code to reach a visitor's browser.
Cleanup on SPA navigation
On single-page applications, when the visitor navigates away from a page where an experiment is running, A vs B automatically:
- Removes the
<style>tag containing the variation's CSS. - Cancels all timers, intervals, listeners, and
waitUntilobservers started via theoptionshelpers. - Runs any callbacks registered with
options.onRemove.
Note that automatic teardown does not undo DOM text or attribute changes your code made: only the resources started through the options helpers are auto-cleaned. If your variation mutated the DOM directly, register an options.onRemove callback to reverse those changes.
The Control variation is the original, unmodified experience. It never has CSS or JS injected. A visitor in the Control group sees your page exactly as it would appear without A vs B running at all.
Rotation and resize
Your variation's JavaScript runs once per page view. If a visitor rotates their device or resizes the browser window afterward, initVariation does not run again. Visual, no-code changes behave differently: they re-check the new width and switch to whatever matches, the same way they do on first load. See Responsive Edits for how that works.
If your variation's JavaScript needs to react when the window crosses a size boundary, for example to redo something only relevant on mobile, add your own window.matchMedia listener inside initVariation. That listener keeps working for as long as the variation is active, since it is ordinary code running in the page.
Editor linting
Variation code can be written in plain JavaScript or TypeScript, matching your experiment's language setting. When you save, the browser compiles what you wrote: TypeScript becomes plain JavaScript, and JavaScript passes through unchanged. It sends both your original source and the compiled result to A vs B. The compiled JavaScript, not your original TypeScript, is what runs in a visitor's browser.
The code editor has a linting control with three levels, the same three levels Project JavaScript uses:
- Off: no checking, no underlines.
- On (the default): the editor underlines type and syntax problems as you type. This is advisory; you can still save.
- Strict: turns on stricter TypeScript checks and blocks saving until you fix every reported error.
Linting is only available for TypeScript experiments, and it applies per experiment: each one carries its own separate setting.
One options shape serves variation, trigger, and project code. So options.experimentId and the other context properties above are typed as optional, even though they are always present in variation code. Under Strict lint mode, assigning one straight to a plain string variable fails to type-check. Narrow it first, for example if (options.experimentId) { ... }, before you rely on it as a string.
Your project's shared types, declared once in shared.d.ts, are available here too, with no import needed. See Project JavaScript, Shared types for how to declare them.