Embed script
Open your form's popup from your own code, react to it with events, and restyle it to fit your site. For when you edit your site's code directly.
The embed script that embeds your form also gives developers a small API for popups, events, and styling. This article is a reference for people who edit their site’s code. The popup needs a browser from 2022 on, and older browsers open the form in a new tab instead. If you only want the popup, the steps in the Embed drawer are enough.
Open a popup from your own code
Load the script without data-form, then call window.xoxo.popup. This example omits async so the API is available on the next line:
<a id="subscribe" href="https://xoxo.email/your-slug" target="_blank" rel="noopener">Subscribe</a>
<script src="https://xoxo.email/embed.js"></script>
<script>
const form = window.xoxo.popup("your-slug", { location: "bottom-right" })
document.querySelector("#subscribe").addEventListener("click", (event) => {
event.preventDefault()
form.open()
})
</script>
All options are optional:
| Option | Value |
|---|---|
trigger |
{ type: "click", selector }, { type: "delay", seconds }, { type: "scroll", percent }, or { type: "exit" } |
location |
One of the popup positions |
A click trigger opens from every link to the form, plus whatever selector matches. The selector is optional. Without a trigger, the popup stays closed until you call open().
The returned handle has open(), close(), and destroy(). Use destroy() to remove the popup and its listeners when leaving a view in a client-side app. In a browser without popup support, open() opens the form’s page in a new tab instead, so call it from a click: outside one, the browser’s popup blocker will usually stop it.
Pair a button with an automatic popup
With a delay, scroll, or exit trigger, links to the form go to the form’s page instead of opening the popup. To have a button open it too, pass the trigger and call open() from the button. Swap in any trigger from the table above:
<a id="subscribe" href="https://xoxo.email/your-slug" target="_blank" rel="noopener">Subscribe</a>
<script src="https://xoxo.email/embed.js"></script>
<script>
const form = window.xoxo.popup("your-slug", { trigger: { type: "scroll", percent: 50 } })
document.querySelector("#subscribe").addEventListener("click", (event) => {
event.preventDefault()
form.open()
})
</script>
The popup opens on its own once per page view, and the button opens it any time, even while automatic opens are paused. Use this in place of the code from the Embed drawer, not beside it, so the page has one popup.
Closed popups stay closed for 30 days
After a visitor closes the popup, delay, scroll, and exit triggers don’t open it again for 30 days. Once they subscribe, those triggers never open it again. Click triggers and open() still open it.
The script remembers this in your site’s local storage, under xoxo: plus the form’s slug. It stores when the hold ends and nothing else, and only the script reads it. To see an automatic popup again while you test, clear it from your browser’s console:
localStorage.removeItem("xoxo:your-slug")
Use it from TypeScript
The script ships no type package. Save this declaration as xoxo.d.ts anywhere your tsconfig.json includes, and window.xoxo is typed:
interface Window {
xoxo?: {
popup(
slug: string,
options?: {
trigger?:
| { type: "click"; selector?: string }
| { type: "delay"; seconds: number }
| { type: "scroll"; percent: number }
| { type: "exit" }
location?:
| "top-left"
| "top-center"
| "top-right"
| "center-left"
| "center"
| "center-right"
| "bottom-left"
| "bottom-center"
| "bottom-right"
},
): { open(): void; close(): void; destroy(): void }
}
}
Keep the file free of import and export lines, so TypeScript adds xoxo to the global Window rather than reading the file as a module. window.xoxo is optional because the script loads on its own schedule, so check it before calling, or use window.xoxo! after a synchronous <script> tag.
Listen for events
The script dispatches four events. They bubble up to the document, and their target is the xoxo-popup element, whose data-form is the form’s slug.
| Event | Fires |
|---|---|
xoxo:ready |
Once, after the form has loaded, usually before it opens |
xoxo:open |
Each time the popup opens |
xoxo:submit |
Each time the form accepts a submission |
xoxo:close |
Each time the popup closes |
Register listeners before loading the script:
<script>
document.addEventListener("xoxo:ready", (event) => console.log("Form ready", event.target))
document.addEventListener("xoxo:submit", (event) => console.log("Form submitted", event.target))
</script>
<script async src="https://xoxo.email/embed.js"></script>
Events carry no personal data. xoxo:submit can’t cancel the submission, and it doesn’t mean the subscriber has confirmed their email.
Style the popup
The popup keeps its own styles in a shadow root, so your page’s CSS can’t change it by accident. Three parts are open to your stylesheet on purpose:
| Part | Styles |
|---|---|
xoxo-popup::part(dialog) |
The popup itself |
xoxo-popup::part(card) |
The card holding the form |
xoxo-popup::part(close) |
The close button |
xoxo-popup::part(card) {
border-radius: 0;
}
xoxo-popup::part(dialog) {
max-width: 420px;
}
The form inside is styled in the editor.