# Custom CSS & JavaScript

Drop bespoke CSS and JS into a test or variant when the visual editor isn't enough. Variant-scoped runs only for that variant; test-wide runs for everyone in the test (including the control).

A power-user escape hatch for changes the visual editor can't express. Drop bespoke CSS or JavaScript onto a variant — or onto every visitor in the test — and the SDK injects it on the relevant page loads.

Custom CSS and JavaScript live on the same screen as your variants in the test wizard, but most teams don't need them — the visual editor handles 90% of variant authoring without custom code. Reach for these fields when the change is dynamic, computed, or easier to express as a stylesheet rule than a list of DOM mutations.

The SDK applies custom CSS and JS automatically as part of the same DOM-mutation step that powers visual variants. There's nothing to wire up on your side — paste the code, save the variant, and the SDK takes care of injection and cleanup.

## Two scopes

### Variant-level

Set on a single variant. Runs only for visitors assigned to that variant. This is where the actual difference between variants usually lives — most teams use variant-level custom code, not test-level.

### Test-level

Set on the test itself. Runs for every visitor in the test, including the control. Useful when every variant should get the same supporting code — a CSS reset, an instrumentation snippet, a helper function. Rare in practice.

## How injection works

- **CSS** — injected as a `<style>` tag appended to `<head>`. Idempotent — the same ID is never injected twice. Normal cascade rules apply; more-specific page selectors win unless you bump specificity.
- **JavaScript** — compiled to an executable function and scheduled to run in the next browser macrotask, so it runs immediately after the SDK applies DOM mutations rather than blocking the page becoming visible.
- **Single execution per session** — the SDK tracks an executed flag and skips re-running already-executed JS, even on SPA route changes. CSS is similarly idempotent at the DOM-ID level.
- **Error-isolated** — if your JS throws, the SDK logs `[Otter] JS execution error (id): <error>` and keeps running. Other tests on the same page are unaffected.

> **Power-user discipline:**
>
> - **Default to the visual editor.** Custom code is harder to review, harder to diff, and harder to debug. The visual editor produces typed, versioned changes; custom code is a blob. If you can express the change with the editor, do.
> - **Treat custom JS like production code.** Custom JS runs on real visitors in your real site. Review it the same way you'd review a script tag in your CMS. Owners, admins, and members can author it; viewers cannot.
> - **Make your JS idempotent and event-safe.** The SDK won't re-run your JS within a session, but a reload or refresh-visitor will. Code that attaches event listeners should still guard against double-binding, and code that inserts elements should check for existence first.
> - **Prefer CSS to JS when both work.** A CSS rule that achieves the same visual change is faster, smaller, and easier to roll back. JS is the tool when you need behavior, not appearance.
> - **Don't depend on libraries you don't already load.** The SDK doesn't bring in jQuery, lodash, or anything else. If your variant needs them, your site needs to load them at the top level.

## Frequently asked questions

### When should I reach for custom CSS or JS instead of the visual editor?

Use the visual editor for almost everything — it's faster, safer, and produces snapshot-able change records. Reach for custom CSS or JS when (a) the change is too dynamic to express as static DOM mutations (animations, conditional logic), (b) you need to compute something from the page state before rendering, or (c) you want to apply a CSS rule that targets many elements at once without authoring a change per element.

### How do I preview my custom CSS/JS before launching the test?

Use the Preview Bar — click 'Preview on site' from the test wizard's Variants step, the test's page, or the edit screen. It opens your real site with a bar at the bottom where you pick the test and variant, and your custom CSS and JS are applied exactly as the live runtime would apply them — drafts included, and nothing is recorded against your results. Note that a draft test is never served to real visitors: launching the test is still required before anyone else sees it.

### What's the difference between test-level and variant-level custom code?

Test-level custom code (set on the test itself) runs for every variant in the test, including the control. Use it when you want everyone in the experiment to get the same code — for example, a CSS reset that applies to all variants. Variant-level custom code runs only for visitors assigned to that specific variant. Most teams use variant-level for the actual difference between variants and rarely use test-level.

### When does the custom JS run during page load?

The SDK schedules it via setTimeout(0) — so it runs in the next macrotask after the SDK applies DOM changes, not synchronously during initialization. This is intentional so that long-running custom JS can't block the page becoming visible. If you need to manipulate the DOM, the elements are already in place; if you need to listen for events, attach the listener as your first action.

### Does custom JS run more than once per page load?

No. The SDK tracks an execution flag per (test, scope) and skips re-execution if already run. So if your code attaches an event listener, you won't end up with duplicate listeners. On a single-page-app route change, the SDK's re-evaluation does NOT re-run already-executed custom JS — the flag persists for the session.

### What happens if my custom JS throws an error?

The SDK catches it and logs `[Otter] JS execution error (id): <error>` in the browser console. The rest of the page keeps working, the SDK keeps running, and other tests on the same page are unaffected. Errors here don't propagate.

### How does custom CSS get applied?

The SDK injects a <style> tag into <head> with a deterministic ID. Variant-scoped CSS gets id=`optimo-css-<test-id>`; test-wide CSS gets id=`optimo-campaign-css-<test-id>`. Injection is idempotent — if a tag with that ID already exists, the SDK skips re-injection. CSS specificity follows normal cascade rules; if your custom CSS doesn't seem to apply, the most common cause is a more-specific selector on the page winning.

### What happens to my custom CSS/JS if the visitor's assignment changes?

Assignment is sticky for the lifetime of the visitor cookie, so this shouldn't normally happen. If you do force-clear the visitor (?optimo-refresh) or switch via preview mode, the SDK removes the previous test's injected CSS and clears the JS execution flag. CSS comes off cleanly; JS that already ran can't be 'unrun' — only the flag is reset, so it could potentially run again on a reload.

### Can I use external libraries (jQuery, lodash) in custom JS?

Only if they're already loaded on the page. The SDK doesn't load anything for you. If your custom JS depends on jQuery, your site needs to load jQuery itself — the variant code can then reference window.jQuery directly. For most cases, plain vanilla DOM APIs are simpler and faster.

### How is this different from just adding a <script> tag to my page?

Functionally similar — both run code in your page context with full access to the DOM. The differences are: (1) the SDK only runs the code for visitors in the relevant variant or test, (2) the code is versioned alongside the test, (3) execution is idempotent within a session, (4) cleanup on assignment-change happens automatically. You wouldn't add a script tag conditionally per A/B variant; custom JS does that for you.

### Is this risky from a security perspective?

It runs arbitrary code in your page — the same risk surface as any inline script you write yourself. The risk is bounded by who can author tests: only owners, admins, and members can create or edit experiments. Viewers cannot author custom JS. Treat custom JS the same way you'd treat any other code that ships to production: review before launching, prefer the visual editor when it suffices, and audit older tests periodically.

### Will custom JS bypass the anti-flicker style?

No, the other way around — by the time your custom JS runs (via setTimeout(0) after the SDK's DOM mutations), the anti-flicker style is being removed and the page is becoming visible. If your custom JS needs to do something before the page paints, do it inside the anti-flicker window by attaching to DOMContentLoaded or by manipulating elements with display:none that you then reveal.

---

Canonical page: https://www.otterab.com/docs/building-tests/custom-css-and-js
