Visual Editor

Make visual changes to your site without writing code. The Visual Editor lets you modify colors, text, and visibility for experiment variants directly from the CADENCE dashboard.

How it works

The Visual Editor loads your site in an iframe and injects a selection script. You click elements to select them, configure changes in the sidebar, and save. The SDK applies those changes automatically when users visit your site.

The flow:

  1. You open the editor for a specific variant of an experiment
  2. Your site loads in an iframe (using the experiment's target URL)
  3. A selection script is injected that highlights elements on hover
  4. You click an element — its CSS selector is generated and sent to the sidebar
  5. You add a mutation (CSS change, HTML replacement, or hide)
  6. The change previews immediately in the iframe
  7. You save — mutations are stored on the variant in the database
  8. At runtime, the SDK fetches the variant config and applies mutations to the DOM

Opening the editor

  1. Navigate to an experiment that has a target URL set
  2. On the experiment detail page, find the Visual Editor card
  3. Click Edit Visually next to the variant you want to modify
  4. The editor opens full-screen with your site in the iframe

Target URL required

The Visual Editor needs to know which page to load. Set a target URL when creating or editing an experiment. This is the page that loads in the iframe.

Selecting elements

Hover over any element on the page — it highlights with a blue outline. Click to select it.

When you click an element, the editor:

  1. Generates a CSS selector using this priority:
    • ID selector#cta-button (most stable)
    • Class-based path.hero > h1.title (filters out dynamic class names from CSS-in-JS)
    • Tag + nth-of-typediv > p:nth-of-type(2) (fallback when no ID or meaningful classes exist)
  2. Captures the element's current computed styles (color, background, font size, display)
  3. Captures the first 200 characters of text content
  4. Sends this data to the sidebar via postMessage

The sidebar then shows the element's tag, selector, and current styles so you can see exactly what you're about to change.

Filtering dynamic classes

The selector generator automatically skips class names that look like they were generated by CSS-in-JS libraries — patterns like css-1a2b3c, sc-bdnxRM, or jsx-abc123. These change between builds and would break your selectors.

Mutation types

CSS mutations

Change visual properties of any element. The sidebar exposes four properties:

| Property | Input | Example | |----------|-------|---------| | Text color | Color picker + hex input | #ffffff | | Background color | Color picker + hex input | #3b82f6 | | Font size | Text input | 18px, 1.2rem | | Display | Dropdown | block, flex, none, inline-block |

Only properties you actually change are included in the mutation. If you only set background color, only background-color is saved.

Under the hood, CSS mutations are applied as inline styles via element.style.setProperty(), so they override stylesheet rules.

HTML mutations

Replace the inner HTML of an element. Type the replacement content in the sidebar's textarea.

Use this to:

  • Change headline text ("Sign Up" → "Start Free Trial")
  • Swap content blocks
  • Update link text or descriptions

HTML mutations replace everything inside the element

The entire innerHTML is replaced, including child elements. Select the most specific element possible — click the span inside a button, not the button itself, if you only want to change text.

Hide mutations

Hide an element by setting display: none. No additional input needed — just select the element, choose "Hide," and add the mutation.

Use this to:

  • Remove distracting elements (banners, popups)
  • Test whether removing a feature improves conversions
  • Simplify a page layout for a variant

Manual selector input

If clicking elements doesn't work (see CORS section below), you can type a CSS selector directly in the sidebar input field.

Tips for writing selectors:

  • Use IDs when available#signup-form is the most reliable
  • Prefer specific paths.hero h1 is better than just h1
  • Test in DevTools first — Open your site in another tab, run document.querySelector('.your-selector') in the console to verify it matches
  • Avoid generated class names — Skip anything that looks like css-1a2b3c or sc-xyz

Managing mutations

All mutations for the current variant are listed in the sidebar under Mutations. From here you can:

  • Click a mutation to select and edit it
  • Delete a mutation with the remove button
  • See an "Unsaved changes" indicator in the toolbar when edits haven't been saved

Multiple mutations can target the same element:

  • CSS mutations merge — each property is applied independently
  • HTML mutations replace — only the last one applies (since each overwrites innerHTML)
  • Hide mutations always win — display: none overrides everything

Saving

Click Save in the toolbar. The editor sends the full mutations array to the API:

POST /api/variants/{variantId}/mutations
{ "mutations": [ ... ] }

Mutations are stored in the variant's feature_overrides column as JSON:

json
{
  "visual_mutations": [
    {
      "id": "mut_1707300000000",
      "selector": "#cta-button",
      "type": "css",
      "css": { "background-color": "#3b82f6", "color": "#ffffff" }
    },
    {
      "id": "mut_1707300001000",
      "selector": ".promo-banner",
      "type": "hide"
    }
  ]
}

Saves are atomic — the entire array is replaced, not individual mutations patched.

How the SDK applies mutations

When a user visits your site and your code calls getVariant():

  1. The SDK looks up the assigned variant
  2. If the variant has visual_mutations, it schedules them via requestAnimationFrame
  3. For each mutation, it calls document.querySelector(selector) and applies the change:
    • CSS: element.style.setProperty(prop, value) for each property
    • HTML: element.innerHTML = html
    • Hide: element.style.display = 'none'
  4. After mutations are applied, showContent() makes the page visible (if anti-flicker is enabled)

If a selector doesn't match any element (e.g., the page changed since the mutation was created), the mutation is silently skipped with a console warning.

Anti-flicker

When visual mutations change visible elements, users may see the original content flash before the variant loads. Prevent this with enableAntiFlicker():

typescript
cadence.enableAntiFlicker(2000)  // Hide page for up to 2 seconds
await cadence.ready()
cadence.getVariant('my-visual-test')  // Mutations applied, page becomes visible

How it works:

  1. enableAntiFlicker() sets document.documentElement.style.visibility = 'hidden'
  2. A safety timeout starts (2 seconds by default)
  3. When getVariant() applies mutations, it calls showContent()visibility: visible
  4. If the SDK fails or times out, the safety timeout restores visibility anyway

Always use anti-flicker with visual mutations

Without it, users will see the control version flash before the treatment loads. Call enableAntiFlicker() before ready() so the page is hidden from the start.

Script tag setup

For non-framework sites, put the anti-flicker call in the <head> so it runs before any content renders:

html
<head>
  <script src="https://unpkg.com/@cadence/sdk"></script>
  <script>
    var cadence = new CadenceClient({ sdkKey: 'YOUR_SDK_KEY' })
    cadence.enableAntiFlicker(2000)
  </script>
</head>

Then in the <body>:

html
<script>
  cadence.ready().then(function () {
    cadence.getVariant('my-visual-test')
    // Page is now visible with mutations applied
  })
</script>

CORS and cross-origin restrictions

This is the most common Visual Editor issue

If your site and the CADENCE dashboard are on different domains, browser security prevents the editor from accessing the iframe's DOM. This is a browser security feature, not a bug.

When it happens

  • Your site is at app.example.com and CADENCE is at cadence.tools
  • Your site is at localhost:8080 and CADENCE is at localhost:3000
  • Any time the protocol, domain, or port differs

What you'll see

  • A yellow "Cross-origin restrictions detected" banner in the editor
  • Your site still loads and displays in the iframe
  • Hovering and clicking elements does nothing (no highlight, no selection)
  • Mutation previews won't render in the iframe

Workarounds

Option 1: Use manual selectors (always works)

Type CSS selectors in the sidebar input instead of clicking elements. Your mutations will still work in production — you just can't preview them in the editor.

Option 2: Run on the same origin (development)

Run both your site and CADENCE on localhost:3000. The SDK's default apiUrl uses the current origin, so this works automatically.

Option 3: Inspect in a separate tab

Open your site in a new browser tab, use DevTools to find the element and copy its selector, then paste it into the manual selector input.

Mutations always work in production

CORS only affects the editor preview. At runtime, the SDK applies mutations to the user's own page (same origin), so there are no cross-origin restrictions.

Limitations

  • DOM-only. Mutations target elements in the initial page DOM. Dynamically loaded content (lazy-loaded sections, SPA route changes) may not be available when mutations run.
  • Inline styles. CSS mutations use inline styles, which have high specificity. They will override most stylesheet rules but may conflict with other inline styles or !important declarations.
  • No script execution. HTML mutations replace innerHTML. Any <script> tags in the replacement HTML will not execute (browser security).
  • SPA considerations. In single-page apps, frameworks may re-render components and recreate DOM nodes after mutations are applied. For SPAs, code-based variants using getVariant() in your components are more reliable than visual mutations.
  • Client-side only. Visual mutations are applied in the browser. They are not visible to search engine crawlers or server-side renderers.
  • Iframe embedding. If your site sends X-Frame-Options: DENY or Content-Security-Policy: frame-ancestors 'none', it won't load in the editor iframe at all.

Best practices

  1. Use ID selectors when possible. They're the most stable across site updates and redesigns.
  2. Keep mutations simple. Color changes, text swaps, and hiding elements work great. Complex layout changes are better done in code.
  3. Always enable anti-flicker for experiments with visual mutations.
  4. Test in multiple browsers. CSS rendering can vary, especially for fonts and colors.
  5. Verify selectors after site updates. A redesign or refactor can break selectors — re-check mutations after shipping frontend changes.
  6. Prefer code-based variants for SPAs. The Visual Editor works best with server-rendered or static HTML pages.
  7. One mutation per element where possible. Multiple mutations on the same element can interact in unexpected ways.

Next steps