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.

Responsive viewport preview

The toolbar contains three viewport buttons — Desktop (monitor icon), Tablet (tablet icon), and Mobile (smartphone icon) — that let you preview how your changes look at different screen sizes.

| Preset | Width | |--------|-------| | Desktop | 1440px | | Tablet | 768px | | Mobile | 375px |

When the selected preset width is wider than the available container, the iframe scales down proportionally (using CSS transform: scale) so the full layout is visible without horizontal scrolling.

A status bar below the iframe shows the current preset name and pixel width — for example, "Desktop — 1440px".

Editing is only available at Desktop viewport

Element selection and mutation editing are disabled at Tablet and Mobile viewports. The sidebar shows a "View only at this breakpoint" notice — switch back to Desktop to add or edit mutations.

Mutations apply to all breakpoints

Mutations you create at Desktop apply to all breakpoints. Responsive viewport is for previewing how your changes look, not for creating breakpoint-specific mutations.

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

Reorder mutations

Move an element to a different position on the page. This lets you test layout changes — like moving a testimonial section above the fold or swapping the order of pricing tiers.

  1. Select the element you want to move
  2. Choose the Reorder tab in the sidebar
  3. Click Select Target and click the element you want to move it relative to
  4. Choose Before or After to place it above or below the target
  5. Click Add Mutation

At runtime, the SDK removes the element from its original position and inserts it before or after the target element using insertAdjacentElement().

CSS mutations

Change visual properties of any element. The sidebar exposes 11 properties organized into three sections:

Typography

| Property | Input | Notes | |----------|-------|-------| | Text color | Color picker + hex input | | | Font size | Text input | e.g., 16px, 1.2rem | | Font weight | Dropdown | Normal (400), Medium (500), Semibold (600), Bold (700) | | Font family | Dropdown | Arial, Helvetica, Georgia, Times New Roman, Courier New, Verdana, System UI | | Text align | Button group | Left, Center, Right |

Spacing

| Property | Input | Notes | |----------|-------|-------| | Padding | Text input | CSS shorthand — e.g., 10px or 8px 16px | | Margin | Text input | CSS shorthand — e.g., 10px or 8px 16px |

Appearance

| Property | Input | Notes | |----------|-------|-------| | Background color | Color picker + hex input | | | Opacity | Slider | 0–100% | | Border radius | Slider | 0–50px | | Display | Dropdown | block, inline, 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

Inline text editing

Double-click any text element on the page to edit its text directly in the iframe — no need to type a replacement in the sidebar.

Supported tags: p, h1h6, span, a, button, li, td, th, label, div.

How it works:

  1. Double-click a text element — it becomes contenteditable
  2. Type to replace the content
  3. Click away (blur) to commit — the change is automatically saved as an HTML mutation
  4. Press Escape to cancel and restore the original text

While text editing is active, a "Text editing — click away to save" badge appears in the toolbar, and the sidebar is hidden to avoid confusion.

Text editing requires Desktop viewport and Edit mode

Inline text editing is only available when the viewport is set to Desktop and the editor is in Edit mode (not Preview).

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
  • Reorder mutations move the element — applied before CSS/HTML mutations on the same element

Undo and redo

The editor maintains up to 50 steps of history. Each time you add a mutation, remove a mutation, or commit an inline text edit, a history entry is created.

Toolbar buttons: Undo (↩) and Redo (↪) appear in the toolbar. They are grayed out when no undo or redo history is available.

Keyboard shortcuts:

| Action | Mac | Windows / Linux | |--------|-----|-----------------| | Undo | Cmd+Z | Ctrl+Z | | Redo | Cmd+Shift+Z | Ctrl+Y |

Undo and redo operate on the full mutations list — each step restores the complete state of the mutations array from that point in history.

Reorder mutations and undo

Undo does not reload the iframe for reorder mutations. After undoing a reorder, the iframe preview may show the old position — save and reload the editor to see the corrected state.

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, add data-anti-flicker to your script tag so the page stays hidden until visual changes are applied:

html
<head>
  <script src="https://cdn.softpath.co/sdk/v1/cadence.min.js" data-project="YOUR_PROJECT_ID" data-anti-flicker></script>
</head>

That's it. Visual experiments auto-run on matching pages, and the page becomes visible once mutations are applied (or after 2 seconds, whichever comes first).

If you need manual control over which experiments run, add a second script block:

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