在客户即将离开 Shopify 网站时弹窗促销信息进行挽留 降低弃购
通过 Bilibili 观看本期 Shopify 教程通过 Youtube 观看本期 Shopify 教程
一、介绍
本期教程分享如何在 Shopify 网站中不借助任何插件实现客户挽留弹窗,当客户即将离开网站时弹出折扣促销信息进行挽留,降低弃购,提高订单转化率。

弹窗中的内容主要由以下几个部分组成:
- 促销信息标题和文字
- 折扣码和一键复制
- 带绿色小圆点的简短描述
- 支持自定义链接的跳转按钮
- 支持自定义时间的倒计时

通过这个功能可以挽回即将离开的客户,提供折扣促进订单转化。同时倒计时也能制造紧迫感,让客户不想错失优惠,进而提高销售额。
主要的触发机制是:
- 电脑端检测到鼠标往浏览器标签页区域移动时弹窗
- 手机端由于没有鼠标检测所以设置为延时弹窗(支持自定义时间)
二、添加方法
创建文件
首先创建一个 Snippet:exit-intent-modal.liquid
如果你跟随本期视频教程在 Shopify 后台添加,复制文件名:
exit-intent-modal
代码如下:
<style>
.exit-modal-container * {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.exit-modal-container {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
line-height: 1.6;
color: #374151;
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideIn {
from {
opacity: 0;
transform: scale(0.95) translateY(-10px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.modal-backdrop {
animation: fadeIn 0.3s ease-out;
}
.modal-content {
animation: slideIn 0.3s ease-out;
}
.hidden {
display: none !important;
}
/* 模态框样式 - 限定在 exit-modal-container 作用域内 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
}
.modal-backdrop-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
}
.modal-container {
position: relative;
background: white;
border-radius: 1rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
max-width: 500px;
margin: 0 1rem;
width: 100%;
}
@media (max-width: 768px) {
.modal-container {
max-width: 90%;
}
}
.modal-close-btn {
position: absolute;
top: 1rem;
right: 1rem;
padding: 0.5rem 0.6rem;
padding-top: 0.6rem;
border-radius: 50%;
background: transparent;
border: none;
cursor: pointer;
z-index: 10;
transition: background-color 0.2s;
}
.modal-close-btn:hover {
background: #f3f4f6;
}
.modal-close-icon {
width: 1.25rem;
height: 1.25rem;
color: #6b7280;
}
.modal-header {
background: linear-gradient(
135deg,
#9333ea 0%,
#ec4899 50%,
#ef4444 100%
);
border-radius: 1rem 1rem 0 0;
padding: 2.5rem;
color: white;
position: relative;
overflow: hidden;
}
.modal-header-bg-1 {
position: absolute;
top: 0;
right: 0;
width: 8rem;
height: 8rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
transform: translate(4rem, -4rem);
}
.modal-header-bg-2 {
position: absolute;
bottom: 0;
left: 0;
width: 6rem;
height: 6rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
transform: translate(-3rem, 3rem);
}
.modal-header-content {
position: relative;
}
.modal-header-top {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
}
.modal-icon-container {
padding: 0.75rem;
padding-top: 0.85rem;
padding-bottom: 0.55rem;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
}
.modal-icon {
width: 24px;
height: 24px;
}
.modal-title {
font-size: 24px;
font-weight: 700;
color: #f3f4f6;
}
.modal-badge {
display: flex;
align-items: center;
gap: 0.25rem;
color: #fef3c7;
}
.modal-badge-icon {
width: 1rem;
height: 1rem;
}
.modal-badge-text {
font-size: 14px;
}
.modal-discount {
font-size: 18px;
font-weight: 500;
margin-bottom: 0.5rem;
}
.modal-discount-number {
font-size: 30px;
font-weight: 700;
color: #fde047;
}
.modal-subtitle {
color: #f3f4f6;
font-size: 14px;
}
.modal-body {
padding: 2.5rem;
}
.modal-content-title {
text-align: center;
margin-bottom: 1.5rem;
}
.modal-content-heading {
font-size: 20px;
font-weight: 600;
color: #1f2937;
margin-bottom: 0.5rem;
}
.modal-content-description {
color: #6b7280;
font-size: 14px;
margin-bottom: 1rem;
}
.discount-code-box {
background: linear-gradient(to right, #f9fafb, #f3f4f6);
border: 2px dashed #d1d5db;
border-radius: 0.75rem;
padding: 1rem;
margin-bottom: 1rem;
}
.discount-code {
font-size: 24px;
font-family: "Courier New", monospace;
font-weight: 700;
color: #ee476a;
letter-spacing: 0.1em;
}
.copy-button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
border: none;
cursor: pointer;
transition: all 0.2s;
background: linear-gradient(to right, #9333ea, #ec4899);
color: white;
}
.copy-button:hover {
background: linear-gradient(to right, #7c3aed, #db2777);
transform: scale(1.05);
}
.copy-button.copied {
background: #10b981;
}
.copy-icon {
width: 1rem;
height: 1rem;
}
.benefits-list {
margin-bottom: 1.5rem;
}
.benefit-item {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 14px;
color: #374151;
margin-bottom: 0.75rem;
}
.benefit-dot {
width: 0.5rem;
height: 0.5rem;
background: #10b981;
border-radius: 50%;
}
.benefit-item .benefit-dot {
display: block;
}
.action-buttons {
display: flex;
gap: 0.75rem;
}
.action-button {
flex: 1;
padding: 0.75rem 1rem;
border-radius: 0.75rem;
font-weight: 500;
border: none;
cursor: pointer;
transition: all 0.2s;
}
.action-button.secondary {
border: 1px solid #d1d5db;
color: #374151;
background: white;
}
.action-button.secondary:hover {
background: #f9fafb;
}
.action-button.primary {
background: linear-gradient(to right, #9333ea, #ec4899);
color: white;
}
.action-button.primary:hover {
background: linear-gradient(to right, #7c3aed, #db2777);
transform: scale(1.05);
}
.timer-section {
margin-top: 2rem;
text-align: center;
}
.timer-text {
font-size: 12px;
color: #6b7280;
}
.countdown {
font-family: "Courier New", monospace;
font-weight: 600;
color: #dc2626;
margin-left: 0.25rem;
}
</style>
<!-- 退出意图模态框 -->
<div id="exitModal" class="modal-overlay hidden exit-modal-container">
<!-- 背景遮罩 -->
<div
class="modal-backdrop-bg modal-backdrop"
onclick="closeModal()"
></div>
<!-- 模态框 -->
<div class="modal-container modal-content">
<!-- 关闭按钮 -->
<button onclick="closeModal()" class="modal-close-btn">
<svg
class="modal-close-icon"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>
</button>
<!-- 带渐变背景的头部 -->
<div class="modal-header">
<div class="modal-header-bg-1"></div>
<div class="modal-header-bg-2"></div>
<div class="modal-header-content">
<div class="modal-header-top">
<div class="modal-icon-container">
<svg
class="modal-icon"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7"
></path>
</svg>
</div>
<div>
<h2 class="modal-title">Wait! Don't Leave!</h2>
<div class="modal-badge">
<svg
class="modal-badge-icon"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"
></path>
</svg>
<span class="modal-badge-text">Limited Time Offer</span>
</div>
</div>
</div>
<p class="modal-discount">
Get <span class="modal-discount-number">20%</span> OFF
</p>
<p class="modal-subtitle">
Don't miss out on this exclusive deal before you go!
</p>
</div>
</div>
<!-- 内容区域 -->
<div class="modal-body">
<div class="modal-content-title">
<h3 class="modal-content-heading">🎉 Exclusive Discount Code</h3>
<p class="modal-content-description">
Copy the discount code below and save 20% instantly!
</p>
<!-- 折扣码框 -->
<div class="discount-code-box">
<div class="discount-code" id="discountCodeDisplay">SAVE20</div>
<button
id="copyBtn"
onclick="copyToClipboard()"
class="copy-button"
>
<svg
id="copyIcon"
class="copy-icon"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
></path>
</svg>
<span id="copyText">Copy Code</span>
</button>
</div>
</div>
<!-- 优惠列表 -->
<div class="benefits-list">
<div class="benefit-item">
<div class="benefit-dot"></div>
<span>Valid on all products</span>
</div>
<div class="benefit-item">
<div class="benefit-dot"></div>
<span>Free shipping on orders $99+</span>
</div>
<div class="benefit-item">
<div class="benefit-dot"></div>
<span>Limited time - today only!</span>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<button onclick="closeModal()" class="action-button secondary">
Maybe Later
</button>
<button onclick="shopNow()" class="action-button primary">
Shop Now
</button>
</div>
<!-- 倒计时 -->
<div class="timer-section">
<p class="timer-text">
⏰ Offer expires in:
<span id="countdown" class="countdown"> 23:59:45 </span>
</p>
</div>
</div>
</div>
</div>
<script>
// ========== 配置变量 ==========
// 设置这些变量来自定义模态框行为
// 是否在每个会话中只显示一次模态框 (true) 或每次满足条件时都显示 (false)
const SHOW_ONCE_PER_SESSION = false;
// 点击"立即购买"按钮时要跳转的URL
const REDIRECT_URL = "https://shopify.now";
// 要复制的折扣码
const DISCOUNT_CODE = "SAVE20";
// 退出意图敏感度(距离屏幕顶部的像素)
const EXIT_SENSITIVITY = 50;
// 检测到退出意图后显示模态框前的延迟(毫秒)
const EXIT_DELAY = 100;
// 移动端延迟(毫秒)- 在移动设备上等待多长时间后显示模态框
const MOBILE_DELAY = 5000;
// ========== 配置结束 ==========
// 退出意图模态框逻辑
class ExitIntentModal {
constructor() {
this.modal = document.getElementById("exitModal");
this.hasShown = false;
this.countdownTimer = null;
this.init();
}
init() {
// 检查此会话中是否已显示过模态框(仅当 SHOW_ONCE_PER_SESSION 为 true 时)
if (
SHOW_ONCE_PER_SESSION &&
sessionStorage.getItem("exitIntentShown") === "true"
) {
this.hasShown = true;
}
// 设置事件监听器
if (this.isMobile()) {
this.setupMobileTimer();
} else {
this.setupDesktopListeners();
}
// 启动倒计时
this.startCountdown();
}
isMobile() {
return (
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
) || window.innerWidth <= 768
);
}
setupDesktopListeners() {
document.addEventListener("mouseleave", (e) => {
if (this.hasShown) return;
// 检查鼠标是否向屏幕顶部移动
if (e.clientY <= EXIT_SENSITIVITY) {
setTimeout(() => {
this.showModal();
}, EXIT_DELAY);
}
});
}
setupMobileTimer() {
if (this.hasShown) return;
setTimeout(() => {
this.showModal();
}, MOBILE_DELAY);
}
showModal() {
if (this.hasShown) return;
this.modal.classList.remove("hidden");
this.hasShown = true;
// 仅当 SHOW_ONCE_PER_SESSION 为 true 时才设置 sessionStorage
if (SHOW_ONCE_PER_SESSION) {
sessionStorage.setItem("exitIntentShown", "true");
}
}
closeModal() {
this.modal.classList.add("hidden");
}
startCountdown() {
// 设置初始时间为从现在开始的两小时
const endTime = new Date().getTime() + 2 * 60 * 60 * 1000;
this.countdownTimer = setInterval(() => {
const now = new Date().getTime();
const distance = endTime - now;
if (distance > 0) {
const hours = Math.floor(
(distance % (1000 * 60 * 60 * 2)) / (1000 * 60 * 60)
);
const minutes = Math.floor(
(distance % (1000 * 60 * 60)) / (1000 * 60)
);
const seconds = Math.floor((distance % (1000 * 60)) / 1000);
const formattedTime = `${this.formatTime(
hours
)}:${this.formatTime(minutes)}:${this.formatTime(seconds)}`;
document.getElementById("countdown").textContent = formattedTime;
} else {
// 倒计时已结束
document.getElementById("countdown").textContent = "00:00:00";
clearInterval(this.countdownTimer);
}
}, 1000);
}
formatTime(time) {
return time.toString().padStart(2, "0");
}
reset() {
this.hasShown = false;
if (SHOW_ONCE_PER_SESSION) {
sessionStorage.removeItem("exitIntentShown");
}
this.closeModal();
}
}
// 初始化模态框
const exitModal = new ExitIntentModal();
// 用于按钮交互的全局函数
function closeModal() {
exitModal.closeModal();
}
function resetModal() {
exitModal.reset();
}
function copyToClipboard() {
const copyBtn = document.getElementById("copyBtn");
const copyIcon = document.getElementById("copyIcon");
const copyText = document.getElementById("copyText");
// 尝试复制到剪贴板
if (navigator.clipboard) {
navigator.clipboard
.writeText(DISCOUNT_CODE)
.then(() => {
showCopiedState();
})
.catch(() => {
fallbackCopy();
});
} else {
fallbackCopy();
}
function fallbackCopy() {
// 旧版浏览器的备用方案
const textArea = document.createElement("textarea");
textArea.value = DISCOUNT_CODE;
document.body.appendChild(textArea);
textArea.select();
document.execCommand("copy");
document.body.removeChild(textArea);
showCopiedState();
}
function showCopiedState() {
// 更新按钮以显示已复制状态
copyBtn.className = "copy-button copied";
copyIcon.innerHTML =
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>';
copyText.textContent = "Copied!";
// 2秒后重置
setTimeout(() => {
copyBtn.className = "copy-button";
copyIcon.innerHTML =
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>';
copyText.textContent = "Copy Code";
}, 2000);
}
}
function shopNow() {
// 首先复制折扣码
copyToClipboard();
// 等待1秒,然后跳转到配置的URL
setTimeout(() => {
// 先关闭模态框
closeModal();
// 跳转到配置的URL
if (REDIRECT_URL && REDIRECT_URL !== "#") {
window.location.href = REDIRECT_URL;
} else {
console.log("No redirect URL configured or URL is #");
}
}, 1000);
}
</script>
引用代码片段
跟随视频教程,在 theme.liquid
中 /body
上方添加:
{% render 'exit-intent-modal' %}
三、自定义配置
主要可以自定义的选项包括:
- 是否只展示一次(为 true 则访问其他页面之后离开不重新展示)
- 折扣码(需替换两处,展示和复制)
- 点击 Shop Now 之后跳转的促销链接
- 移动端延时多少秒弹窗
总之,常用的配置都已经添加注释,对于其他文本内容,直接复制进行搜索,找到之后修改替换即可。
第 135 期 零基础 Shopify 网页修改教程 常用方法演示 优化店铺必看
面向个人卖家和运营人员的 Shopify 零基础前端教程,修改店铺网页内容必看,讲解了如何修改 Shopify 店铺中的网页元素。主要包括如何在商城编辑器中编辑和自定义页面、产品、导航栏等常见元素。同时还介绍了一些高级技巧,如使用 CSS 微调页面布局。

如果你添加到主题之后希望对样式进行微调,可以复制代码提供给 Claude 或 Gemini 结合你自己的需求进行修改。
增强版主题
更适合中国商家的 Shopify 增强版主题,额外添加五十几项功能
Shopify 增强版主题,添加几十项功能,容易上手,持续更新,支持一键开启或关闭。适用于所有 Shopify 推出的 13 款 2.0 主题,减少插件安装,降低每月的插件订阅费。Shopify 店铺用什么主题比较好?哪款主题更好用?
