---
title: Content A/B testing with the visual editor
slug: content-via-visual-editor
description: How content campaigns work in Personizely: A/B test or personalize page content in the visual editor, change types, flicker handling, custom code and conflicts.
image: https://archbee-image-uploads.s3.amazonaws.com/1y1Fr-DMGy_apx7FwibyA/ORDTWfNKMXiY3ZRv0QSgP_personizely-docs-og.png
docTags: 
createdAt: 2026-10-16T19:39:08.774Z
---

A content campaign changes elements on your existing pages, such as headlines, images, buttons, sections and their order, without touching your theme code. You make the changes in Personizely's visual editor, then run them as an A/B test against the original page or as a personalization for a specific audience.

**Available on:** all platforms (Shopify, Shopify Hydrogen, Wix, WordPress and WooCommerce, Webflow, Square, and any site with the script tag). **Plan:** content campaigns must be included in your plan.
**Available as:** A/B test · Personalization

:::hint{type="info"}
**Test or personalize.** You can run a content campaign as an **A/B test**, where visitors are split at random and you measure which version wins, or as a **personalization**, where each variation is shown to a specific audience, with an optional control group to measure the lift. The setup is the same; [Personalizations](https://docs.personizely.net/personalizations) covers what's different.
:::

## How it works

1. **You choose the pages.** A campaign runs on one or more [pages](https://docs.personizely.net/campaign-pages): a single URL, a set of URLs matched by rules or a regular expression, or every page.
2. **You edit each variation in the visual editor.** The editor loads your live site. You select elements and change them. Each change is saved as a *region*: a CSS selector plus the changes to apply to the element it matches. Each variation stores its changes per page.
3. **The visitor is assigned.** When a visitor opens a matching page, Personizely checks the campaign's target and traffic allocation, then assigns them to the control or a variation. The visitor keeps that assignment on later pages and visits. See [How visitors are assigned](https://docs.personizely.net/visitor-assignment).
4. **The changes are applied in the browser.** The snippet finds each region's element and applies the changes. Elements that are about to change stay hidden until the change is in place, so visitors don't see the original content flash. If an element can't be changed quickly (for example, it never appears on the page), it's shown unchanged rather than left hidden.
5. **The view is recorded.** A campaign view is counted once per page load for the visitor's variation, or for the control. Conversions are then [attributed](https://docs.personizely.net/attribution) to the variation.

## Set up a content campaign

1. Go to **Campaigns → New campaign → Content**, and choose **Experiment** or **Personalization**.
2. Under **Pages**, pick an existing page or click **Create a page**. See [Campaign pages and URL matching](https://docs.personizely.net/campaign-pages).
3. Open a variation in the editor and make your changes. Use **Find element** to locate an element by CSS selector, and **Edit code** to add CSS or JavaScript.
4. If the campaign runs on more than one page, switch pages in the editor and make each page's changes.
5. Set the target, goals and (for experiments) the traffic split.
6. Preview each variation with **Preview on the live site**, then start the campaign.

### Campaign options

| Option             | What it does                                                                                                                | Values / default                                                                                                                                    |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| Pages              | Where the campaign runs                                                                                                     | One or more pages                                                                                                                                   |
| Target             | Who can enter the campaign                                                                                                  | All visitors, or a specific [target](https://docs.personizely.net/targets)                                                                          |
| Targeting mode     | **Live** re-checks the target on every page; **Permanent** keeps showing the visitor's variation once they've been targeted | Live (default), Permanent                                                                                                                           |
| Traffic allocation | Share of eligible visitors who enter the campaign                                                                           | 100%                                                                                                                                                |
| Control group      | Share of entering visitors who see the original page                                                                        | 50% for experiments, 25% for personalizations                                                                                                       |
| Trigger            | When the campaign runs                                                                                                      | **Immediate** (runs as the page loads) or **Manual** (runs only when started from the JavaScript API or, for personalizations, a widget automation) |
| Goals              | What counts as a conversion                                                                                                 | At least one goal is required to start                                                                                                              |

The **Trigger** option exists only for content campaigns. A **Manual** campaign runs when you call `ply.runCampaign(campaignId)` from the [JavaScript API](https://docs.personizely.net/js-api), or, for personalizations, when a widget's **Run campaign** [automation](https://docs.personizely.net/automations) fires. Once a visitor has been assigned to a variation, a manual campaign keeps applying it automatically on their later page loads.

## What you can change

| Change                | What it does                                                                                                                                      |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| Text and HTML         | Replaces the element's content. Supports [dynamic text](https://docs.personizely.net/dynamic-text) such as `{visitor.firstName}` or `{geo.city}`. |
| Styles                | Sets inline styles (colors, fonts, spacing, visibility, background image)                                                                         |
| Classes               | Adds or removes CSS classes                                                                                                                       |
| Image or video source | Replaces the `src`. On Shopify, responsive image sizes from the Shopify CDN are kept, and lazy-loaded images are updated too.                     |
| Attributes            | Sets any attribute, for example a link's `href`                                                                                                   |
| Insert element        | Inserts new HTML after an existing element                                                                                                        |
| Rearrange             | Reorders the children of an element, for example sections or list items                                                                           |

### Watching an element for changes

Some themes rewrite elements after the page loads, for example a product title re-rendered when the variant changes. By default a change is applied once. Click the eye icon on a region to watch it: the change is applied again whenever the site rewrites the element, and again if the element is removed and re-added. Don't watch elements whose content legitimately changes on its own, such as cart counts, sliders or live prices.

### Desktop and mobile

To show an element only on one device size, select it in the visual editor and use the device visibility button to show it on desktop only, on mobile only (screens 480 px wide and below), or hide it. This is useful for inserted elements.

### Custom CSS and JavaScript

**Edit code** opens the CSS and JavaScript editors, each with two tabs:

- **Campaign:** code that runs for every variation (not the control), on each page where the variation has changes.
- **Variation:** code that runs for this variation on the current page only.

Variation JavaScript has `variationId` and `pageId` in scope. Both campaign and variation JavaScript get a `helpers` object:

| Helper                                                      | What it does                                                                                                     |
| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `helpers.waitForElement(selector, callback, timeout)`       | Calls `callback(element)` once the first element matching `selector` exists. Optional `timeout` in ms.           |
| `helpers.liveQuerySelectorAll(selector, callback, timeout)` | Calls `callback(element)` for every current and future element matching `selector`, until the optional `timeout` |
| `helpers.swap(parent, a, b)`                                | Moves the child at index `b` of `parent` before the child at index `a`                                           |
| `helpers.readCookie(name)`                                  | Reads a cookie                                                                                                   |
| `helpers.createCookie(name, value, seconds)`                | Writes a cookie; `value` of `null` deletes it                                                                    |

```javascript
// Variation JavaScript: add a badge to every product card, including ones loaded later
helpers.liveQuerySelectorAll('.product-card', (card) => {
  card.insertAdjacentHTML('afterbegin', '<span class="badge">Bestseller</span>')
})
```

Errors in your code don't break the page. They're logged to the console when [debug mode](https://docs.personizely.net/troubleshooting-installation) (`?ply_debug=1`) is on.

## Behavior details and limitations

- **One campaign per element.** If two running campaigns change the same element, the first one to apply its changes wins and the other's changes to that element are skipped. Changes from different campaigns are never merged. Use [campaign groups](https://docs.personizely.net/campaign-groups) to keep overlapping tests apart.
- **Pages are matched on the path.** The domain is ignored, so the same page definition matches on every domain your snippet runs on. See [Campaign pages](https://docs.personizely.net/campaign-pages).
- **A page with no changes still counts.** A visitor assigned to a variation that has no changes on the current page still records a view for that variation.
- **Dynamic text waits for data.** If a change uses `{visitor.*}`, `{geo.*}` or `{company.*}` placeholders, the campaign waits for that data before running, and the affected elements stay hidden meanwhile.
- **Targets that need server data.** Targets using filters such as location or order history are checked once that data has loaded. Affected elements stay hidden until then.
- **Snippet loading affects flicker.** With the snippet loaded asynchronously, the page can paint before the snippet runs, and the original content can briefly show. When you first start a campaign, Personizely offers to switch your site to synchronous loading. See [Performance and flicker](https://docs.personizely.net/performance-anti-flicker).
- **Single-page apps.** On sites that change pages without a full reload, campaigns re-evaluate on each route change and re-apply their changes.
- **Selectors must be stable.** Changes are tied to CSS selectors. If you edit your theme and a selector stops matching, the change silently stops applying. The editor flags regions it can't find on the page.
- **Deploying.** When a content experiment ends, you can [deploy](https://docs.personizely.net/deploy-winners) the winning variation. It's then shown to every visitor who matches the target, with no further tracking, until you undeploy it. Personalizations can't be deployed.

## Running it as a personalization

- **One target per variation, first match wins.** Each variation has its own target, and a visitor gets the first variation in the list whose target they match. Put narrow audiences above broad ones. A visitor who matches no variation sees the original page.
- **Control group.** 25% of matched visitors by default see the original page and are counted as control, so the report can show the lift per variation.
- **Live or Permanent.** With **Live** (the default), targets are checked again on every page, so a visitor can switch variations, for example after logging in. Use **Permanent** to keep the first variation for the rest of the visit when the audience depends on something that changes, such as cart value.
- **Run it from a widget.** Only content personalizations can use the **Manual** trigger with a widget's **Run campaign** [automation](https://docs.personizely.net/automations), for example to change the page after a visitor answers a quiz.
- **No deploy.** Personalizations can't be deployed, so the report has no **Deploy**. A personalization keeps running until you stop it, and you can edit its variations and targets as needed. To stop measuring and show it to every matching visitor, set the control group to 0%.
- **Example audiences:** ad-matched headlines, with one variation per `utm_campaign` value using the [UTM parameters filter](https://docs.personizely.net/utm-parameters); or a different hero for visitors from one country with the [Location filter](https://docs.personizely.net/location).

## Examples

- **Headline test on the homepage:** a **Single page** for `/`, two variations with different hero headlines and button text, primary goal **Purchase** with revenue per visitor.
- **Trust badges on all product pages:** a **Multiple pages** page with the rule *Include pages starting with `/products/`*, and a variation that inserts a badge row after the add-to-cart button.
- **Returning-customer messaging:** a personalization on the cart page whose variation targets returning customers and changes the free-shipping banner text, with a 25% control group.

## Troubleshooting

- **The original content flashes before the change** → the snippet loads asynchronously. Switch to synchronous loading or move the snippet higher in the page. See [Performance and flicker](https://docs.personizely.net/performance-anti-flicker).
- **A change applies, then reverts** → the theme rewrites the element after load. Turn on watching (eye icon) for that region.
- **A change never applies** → the selector no longer matches, or another campaign already changed the element. Open the page with `?ply_debug=1` and check the console.
- **The campaign doesn't run on a page** → the page definition doesn't match the URL (check **Include query**), the visitor isn't in the target, or they're outside the traffic allocation.

## Related

- [Campaign pages and URL matching](https://docs.personizely.net/campaign-pages)
- [A/B tests](https://docs.personizely.net/experiments-ab-tests)
- [Personalizations](https://docs.personizely.net/personalizations)
- [Performance and flicker](https://docs.personizely.net/performance-anti-flicker)
- [Preview and QA](https://docs.personizely.net/preview-qa)
- [Dynamic text](https://docs.personizely.net/dynamic-text)
