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:
- You open the editor for a specific variant of an experiment
- Your site loads in an iframe (using the experiment's target URL)
- A selection script is injected that highlights elements on hover
- You click an element — its CSS selector is generated and sent to the sidebar
- You add a mutation (CSS change, HTML replacement, or hide)
- The change previews immediately in the iframe
- You save — mutations are stored on the variant in the database
- At runtime, the SDK fetches the variant config and applies mutations to the DOM
Opening the editor
- Navigate to an experiment that has a target URL set
- On the experiment detail page, find the Visual Editor card
- Click Edit Visually next to the variant you want to modify
- 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:
- 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-type —
div > p:nth-of-type(2)(fallback when no ID or meaningful classes exist)
- ID selector —
- Captures the element's current computed styles (color, background, font size, display)
- Captures the first 200 characters of text content
- 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-formis the most reliable - Prefer specific paths —
.hero h1is better than justh1 - 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-1a2b3corsc-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: noneoverrides 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:
{
"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():
- The SDK looks up the assigned variant
- If the variant has
visual_mutations, it schedules them viarequestAnimationFrame - 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'
- CSS:
- 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():
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:
enableAntiFlicker()setsdocument.documentElement.style.visibility = 'hidden'- A safety timeout starts (2 seconds by default)
- When
getVariant()applies mutations, it callsshowContent()→visibility: visible - 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:
<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>:
<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.comand CADENCE is atcadence.tools - Your site is at
localhost:8080and CADENCE is atlocalhost: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
!importantdeclarations. - 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: DENYorContent-Security-Policy: frame-ancestors 'none', it won't load in the editor iframe at all.
Best practices
- Use ID selectors when possible. They're the most stable across site updates and redesigns.
- Keep mutations simple. Color changes, text swaps, and hiding elements work great. Complex layout changes are better done in code.
- Always enable anti-flicker for experiments with visual mutations.
- Test in multiple browsers. CSS rendering can vary, especially for fonts and colors.
- Verify selectors after site updates. A redesign or refactor can break selectors — re-check mutations after shipping frontend changes.
- Prefer code-based variants for SPAs. The Visual Editor works best with server-rendered or static HTML pages.
- One mutation per element where possible. Multiple mutations on the same element can interact in unexpected ways.
Next steps
- Script Tag Setup — Complete guide for non-framework sites (ideal for visual mutations)
- Creating Tests — Set up experiments with or without the Visual Editor
- API Reference —
enableAntiFlicker()andgetVariant()details - Troubleshooting — Fix Visual Editor issues